[09/11] ipa: libipa: Introduce LscAlgorithm
diff mbox series

Message ID 20260615-libipa-algorithms-v1-9-e949c937422e@ideasonboard.com
State Superseded
Headers show
Series
  • ipa: libipa: Introduce libipa algorithms
Related show

Commit Message

Jacopo Mondi June 15, 2026, 2:05 p.m. UTC
Introduce an Lsc algorithm implementation in libIPA.

The LscAlgorithm class supports two algorithms: grid-based lsc and
polynomial-based lsc.

Both implementations have been generalized from the existing ones in the
RkISP1 IPA.

Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
---
 src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++
 src/ipa/libipa/lsc.h              | 145 +++++++++++++
 src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++
 src/ipa/libipa/lsc_base.h         |  59 ++++++
 src/ipa/libipa/lsc_grid.cpp       |  33 +++
 src/ipa/libipa/lsc_grid.h         | 113 ++++++++++
 src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-
 src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----
 src/ipa/libipa/meson.build        |   5 +
 src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------
 src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--
 src/ipa/rkisp1/ipa_context.h      |  10 +-
 12 files changed, 1126 insertions(+), 450 deletions(-)

Comments

Kieran Bingham June 16, 2026, 2 p.m. UTC | #1
Quoting Jacopo Mondi (2026-06-15 15:05:34)
> Introduce an Lsc algorithm implementation in libIPA.
> 
> The LscAlgorithm class supports two algorithms: grid-based lsc and
> polynomial-based lsc.
> 
> Both implementations have been generalized from the existing ones in the
> RkISP1 IPA.
> 
> Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> ---
>  src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++
>  src/ipa/libipa/lsc.h              | 145 +++++++++++++
>  src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++
>  src/ipa/libipa/lsc_base.h         |  59 ++++++
>  src/ipa/libipa/lsc_grid.cpp       |  33 +++
>  src/ipa/libipa/lsc_grid.h         | 113 ++++++++++
>  src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-
>  src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----
>  src/ipa/libipa/meson.build        |   5 +
>  src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------
>  src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--
>  src/ipa/rkisp1/ipa_context.h      |  10 +-
>  12 files changed, 1126 insertions(+), 450 deletions(-)
> 
> diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
> new file mode 100644
> index 000000000000..ae713a776778
> --- /dev/null
> +++ b/src/ipa/libipa/lsc.cpp
> @@ -0,0 +1,322 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026 Ideas on Board Oy
> + *
> + * libIPA Lsc algorithms
> + */
> +
> +#include "lsc.h"
> +
> +#include <libcamera/base/log.h>
> +
> +/**
> + * \file lsc.h
> + * \brief libipa lsc algorithm
> + */
> +
> +namespace libcamera {
> +
> +LOG_DEFINE_CATEGORY(Lsc)
> +
> +namespace ipa {
> +
> +namespace lsc {
> +
> +/**
> + * \struct ActiveState
> + * \brief The lsc active state
> + *
> + * \var ActiveState::enabled
> + * \brief Boolean flag for the LscAlgorithm enable status
> + */
> +
> +/**
> + * \struct FrameContext
> + * \brief The lsc frame context
> + *
> + * \var FrameContext::enabled
> + * \brief Boolean flag for the LscAlgorithm enable status
> + *
> + * \var FrameContext::update
> + * \brief Boolean flag for the LscAlgorithm updated status
> + */
> +
> +} /* namespace lsc */
> +
> +#ifndef __DOXYGEN__
> +template<>
> +void Interpolator<lsc::Components<uint16_t>>::
> +       interpolate(const lsc::Components<uint16_t> &a,
> +                   const lsc::Components<uint16_t> &b,
> +                   lsc::Components<uint16_t> &dest,
> +                   double lambda)
> +{
> +       for (auto const &[k, v] : a)
> +               interpolateVector(v, b.at(k), dest[k], lambda);
> +}
> +#endif
> +
> +/**
> + * \class LscAlgorithmBase
> + * \brief Base class for LscAlgorithm
> + *
> + * Base class for LscAlgorithm for non-templated functions implementation
> + */
> +
> +/**
> + * \brief Queue a request to the lsc algorithm
> + * \param[in] state The lsc active state
> + * \param[in] context The lsc frame context
> + * \param[in] controls The list of controls associated with a Request
> + *
> + * Queue a new list of \a controls to the lsc algorithm.
> + * The only supported control is controls::LensShadingCorrectionEnable.
> + */
> +void LscAlgorithmBase::queueRequest(lsc::ActiveState &state,
> +                                   lsc::FrameContext &context,
> +                                   const ControlList &controls)
> +{
> +       const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);
> +       if (lscEnable && *lscEnable != state.enabled) {
> +               state.enabled = *lscEnable;
> +
> +               LOG(Lsc, Debug)
> +                       << (state.enabled ? "Enabling" : "Disabling") << " Lsc";
> +
> +               context.update = true;
> +       }
> +
> +       context.enabled = state.enabled;
> +}
> +
> +/**
> + * \brief Populate the list of lsc metadata
> + * \param[in] context The lsc frame context
> + * \param[in] metadata The list of metadata
> + *
> + * Populates the list of \a metadata with controls handled by the LscAlgorithm
> + * class. The only supported metadata is controls::LensShadingCorrectionEnable.
> + */
> +void LscAlgorithmBase::process(lsc::FrameContext &context, ControlList &metadata)
> +{
> +       metadata.set(controls::LensShadingCorrectionEnable, context.enabled);
> +}
> +
> +/**
> + * \class LscAlgorithm
> + * \brief libIPA lsc algorithm implementation
> + * \tparam T The type used to store the gain values when loaded from tuning file
> + * \tparam U The fixedpoint lsc engine register format
> + *
> + * Due to the optical characteristics of the lens, the light intensity received
> + * by the sensor is not uniform. The Lens Shading Correction algorithm applies
> + * multipliers to all pixels to compensate for the lens shading effect.
> + *
> + * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.
> + * It provides support for parsing the tuning file content and generate tables
> + * of per-colour temperature gains that IPA algorithms can use to program their
> + * lsc engine.
> + *
> + * The init() function parses the tuning file and loads the gain tables either
> + * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain
> + * tables are organized per-colour temperature with per-colour components gain
> + * vectors or polynomial coefficients. The colour components names are
> + * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc
> + * engine support 4 colour components (r, gr, gb, b), some only support 3 colour
> + * components (r, g, b). The name (and number) of the expected colour components
> + * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys
> + * field.
> + *
> + * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour
> + * components. The gain table has been here omitted, but the expected number
> + * of entries has be equal to
> + * LscDescriptor::numHCells * LscDescriptor::numVCells.
> + *
> + * \code{.yaml}
> + * - Lsc:
> + *    sets:
> + *      - ct: 2500
> + *        r: [
> + *             .. gains table omitted..
> + *        ]
> + *        g: [
> + *             .. gains table omitted..
> + *        ]
> + *        b: [
> + *             .. gains table omitted..
> + *        ]
> + *      - ct: 6500
> + *        r: [
> + *             .. gains table omitted..
> + *        ]
> + *        g: [
> + *             .. gains table omitted..
> + *        ]
> + *        b: [
> + *             .. gains table omitted..
> + *        ]
> + * \endcode
> + *
> + * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'
> + * colour components:
> + *
> + * \code{.yaml}
> + * - Lsc:
> + *    type: "polynomial"
> + *    sets:
> + *      - ct: 2500
> + *        r:
> + *          cx: 0.5006571711950275
> + *          cy: 0.510093737499277
> + *          k0: 1.5393282208428813
> + *          k1: -1.1434559757908016
> + *          k2: 4.332602305814554
> + *          k3: 0.0
> + *          k4: 0.0
> + *        gr:
> + *          cx: 0.5009320529087338
> + *          cy: 0.511208038949085
> + *          k0: 1.5634738574805407
> + *          k1: -1.5623484259968348
> + *          k2: 4.846686073656501
> + *          k3: 0.0
> + *          k4: 0.0
> + *        gb:
> + *          cx: 0.5012013290343839
> + *          cy: 0.5128251541578288
> + *          k0: 1.526147944919103
> + *          k1: -1.4316976083689723
> + *          k2: 4.792604063222728
> + *          k3: 0.0
> + *          k4: 0.0
> + *        b:
> + *          cx: 0.49864139511067784
> + *          cy: 0.5162095081739346
> + *          k0: 1.0405245474038738
> + *          k1: 0.05618339879447103
> + *          k2: 1.8792813594001752
> + *          k3: 0.0
> + *          k4: 0.0
> + *      - ct: 6000
> + *        r:
> + *          cx: 0.5006202239353942
> + *          cy: 0.5099531318307661
> + *          k0: 1.4702946023945032
> + *          k1: -0.8893767547927631
> + *          k2: 3.920547732201387
> + *          k3: 0.0
> + *          k4: 0.0
> + *        gr:
> + *          cx: 0.500907874178317
> + *          cy: 0.511084916024106
> + *          k0: 1.5336172760559457
> + *          k1: -1.39964026514435
> + *          k2: 4.565487728954618
> + *          k3: 0.0
> + *          k4: 0.0
> + *        gb:
> + *          cx: 0.5011898608900477
> + *          cy: 0.5126797906745105
> + *          k0: 1.5013145790354843
> + *          k1: -1.2747407173754124
> + *          k2: 4.514682876897286
> + *          k3: 0.0
> + *          k4: 0.0
> + *        b:
> + *          cx: 0.4987561413116136
> + *          cy: 0.5159619420778772
> + *          k0: 1.0102986422191802
> + *          k1: 0.13263449763985727
> + *          k2: 1.686556107316064
> + *          k3: 0.0
> + *          k4: 0.0
> + * \endcode
> + *
> + * The lsc tables or the polynomial definition are generated at tuning time
> + * using an image of known resolution which needs to be specified in
> + * LscDescriptor::sensorSize.
> + *
> + * At LscAlgorithm::configure() time the lsc tables are re-sampled on the
> + * sensor's crop rectangle in use to adapt them to the configuration in use for
> + * a streaming session. Polynomial lsc tables support re-sampling and can be
> + * applied to any sensor configuration. Grid-based lsc tables cannot be

'cannot currently be re-sampled'.

And perhaps a \todo: Implement grid based resamplling

> + * re-sampled and the configuration as parsed from the tuning file is used for
> + * all sensor configurations providing best-effort results.
> + *
> + * When the IPA algorithms wants to get access to the (resampled) tables to
> + * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get
> + * an lsc table interpolated by the LscAlgorithm class for the specified colour
> + * temperature. If the algorithm wants to access the non-interpolated tables it
> + * can retrieve them using LscAlgorithm::getComponents().
> + */
> +
> +/**
> + * \fn LscAlgorithm::init()
> + * \param[in] tuningData The tuning data
> + * \param[in] controls The IPA list of supported controls
> + * \param[in] descriptor The lsc engine descriptor
> + *
> + * Parse \a tuningData according to the settings specified in \a descriptor to
> + * populate the lsc data and registers lsc controls in \a controls.
> + *
> + * \return 0 on success, a negative error code otherwise
> + */
> +
> +/**
> + * \fn LscAlgorithm::configure()
> + * \param[in] state The lsc active state
> + * \param[in] analogCrop The current sensor analog crop rectangle
> + * \param[in] xPos List of horizontal positions of the LSC grid nodes
> + * \param[in] yPos List of vertical positions of the LSC grid nodes
> + *
> + * Re-sample the lsc data for an \a analogCrop.
> + *
> + * Lsc data are generated at tuning time using a known sensor configuration.

Lsc data s/are/is/ generated

> + * When a new streaming session is started, it might use a different sensor
> + * configuration for which the lsc tables need to be adjusted to.
> + *
> + * This function re-generates the lsc tables to adapt them to a new sensor
> + * configuration, specifically it re-samples the lsc data for a new \a
> + * analogCrop on a grid specified by \a xPos and \a yPos. Re-sampling of
> + * lsc data is only supported by polynomial-based lsc tables.

only s/ / currently / supported.

We shouldn't imply that grid based data can't be resampled. it's just
that we haven't done that so far.

At some point grid-resampling is likely to become necessary.

> + *
> + * \sa LscImplementation::resampleLscData
> + *
> + * \return 0 on success, a negative error code otherwise
> + */
> +
> +/**
> + * \fn LscAlgorithm::interpolateComponents
> + * \brief Interpolate the lsc tables for a given colour temperature
> + * \param[in] ct The colour temperature
> + *
> + * Lsc data are generated using different colour temperatures during the

s/are/is/

> + * tuning phase.
> + *
> + * This function returns the interpolated lsc data for a given \a ct
> + * colour temperature.
> + *
> + * IPA algorithm can use this function to obtain a list of gains per-colour
> + * component to program their lsc engines with every time a significant enough
> + * change in colour temperature is detected.
> + *
> + * Calling this function is only valid after LscAlgorithm::configure() has been
> + * called. An empty components list is returned otherwise.
> + *
> + * \return The lsc gains table interpolated for temperature \a ct
> + */
> +
> +/**
> + * \fn LscAlgorithm::getComponents
> + *
> + * Return the map of lsc data per colour temperature.
> + *
> + * Calling this function is only valid after LscAlgorithm::configure() has been
> + * called. An empty components list is returned otherwise.
> + *
> + * \return The map of lsc gains tables per colour-temperature
> + */
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc.h b/src/ipa/libipa/lsc.h
> new file mode 100644
> index 000000000000..8bfa3a7c4ecf
> --- /dev/null
> +++ b/src/ipa/libipa/lsc.h
> @@ -0,0 +1,145 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026 Ideas on Board Oy
> + *
> + * libIPA Lsc algorithms
> + */
> +
> +#pragma once
> +
> +#include <memory>
> +
> +#include <libcamera/control_ids.h>
> +
> +#include "interpolator.h"
> +#include "lsc_grid.h"
> +#include "lsc_polynomial.h"
> +
> +namespace libcamera {
> +
> +LOG_DECLARE_CATEGORY(Lsc)
> +
> +namespace ipa {
> +
> +namespace lsc {
> +
> +struct ActiveState {
> +       bool enabled;
> +};
> +
> +struct FrameContext {
> +       bool enabled;
> +       bool update;
> +};
> +
> +} /* namespace lsc */
> +
> +#ifndef __DOXYGEN__
> +template<typename T>
> +void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,
> +                      std::vector<T> &dest, double lambda)
> +{
> +       ASSERT(a.size() == b.size());
> +       dest.resize(a.size());
> +       for (size_t i = 0; i < a.size(); i++)
> +               dest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;
> +}
> +
> +template<>
> +void Interpolator<lsc::Components<uint16_t>>::
> +       interpolate(const lsc::Components<uint16_t> &a,
> +                   const lsc::Components<uint16_t> &b,
> +                   lsc::Components<uint16_t> &dest,
> +                   double lambda);
> +#endif /* __DOXYGEN__ */
> +
> +class LscAlgorithmBase
> +{
> +public:
> +       void queueRequest(lsc::ActiveState &state, lsc::FrameContext &context,
> +                         const ControlList &controls);
> +       void process(lsc::FrameContext &context, ControlList &metadata);
> +};
> +
> +template<typename T, typename U>
> +class LscAlgorithm : public LscAlgorithmBase
> +{
> +public:
> +       int init(const ValueNode &tuningData, ControlInfoMap::Map &controls,
> +                LscDescriptor descriptor)
> +       {
> +               polynomial_ = false;
> +
> +               std::string type = tuningData["type"].get<std::string>("table");
> +               if (type == "table") {
> +                       impl_ = std::make_unique<LscGrid<T, U>>();
> +                       LOG(Lsc, Debug) << "Using grid-based Lsc";
> +               } else if (type == "polynomial") {
> +                       impl_ = std::make_unique<LscPolynomial<T, U>>();
> +                       polynomial_ = true;
> +                       LOG(Lsc, Debug) << "Using polynomial Lsc";
> +               } else {
> +                       LOG(Lsc, Error) << "Unsupported Lsc algorithm '"
> +                                       << type << "'";
> +                       return -EINVAL;
> +               }
> +
> +               const ValueNode &yamlSets = tuningData["sets"];
> +               if (!yamlSets.isList()) {
> +                       LOG(Lsc, Error) << "'sets' parameter not found in tuning file";
> +                       return -EINVAL;
> +               }
> +
> +               int ret = impl_->parseLscData(yamlSets, descriptor);
> +               if (ret)
> +                       return ret;
> +
> +               controls[&controls::LensShadingCorrectionEnable] =
> +                       ControlInfo(false, true, true);
> +
> +               return 0;
> +       }
> +
> +       int configure(lsc::ActiveState &state, const Rectangle &analogCrop,
> +                     const std::vector<double> &xPos,
> +                     const std::vector<double> &yPos)
> +       {
> +               LOG(Lsc, Debug) << "Sample Lsc data for " << analogCrop;
> +               lsc::ComponentsMap<T> lscData =
> +                       impl_->resampleLscData(analogCrop, xPos, yPos);
> +
> +               /*
> +                * Retain a copy of the components table.
> +                *
> +                * We could avoid a copy here if getComponents() could
> +                * return sets_.data() but I wasn't able to work around the
> +                * compiler refusing it.
> +                */
> +               lscData_ = lscData;
> +
> +               sets_.setData(std::move(lscData));
> +               state.enabled = true;
> +
> +               return 0;
> +       }
> +
> +       const lsc::Components<T> interpolateComponents(unsigned int ct)
> +       {
> +               return sets_.getInterpolated(ct);
> +       }
> +
> +       const lsc::ComponentsMap<T> getComponents()
> +       {
> +               return lscData_;
> +       }
> +
> +private:
> +       std::unique_ptr<LscImplementation<T, U>> impl_;
> +       Interpolator<lsc::Components<T>> sets_;
> +       lsc::ComponentsMap<T> lscData_;
> +       bool polynomial_;
> +};
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_base.cpp b/src/ipa/libipa/lsc_base.cpp
> new file mode 100644
> index 000000000000..fba8d2f00d54
> --- /dev/null
> +++ b/src/ipa/libipa/lsc_base.cpp
> @@ -0,0 +1,152 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026 Ideas on Board Oy
> + *
> + * Base classes and types for LSC algorithms implementations
> + */
> +
> +#include "lsc_base.h"
> +
> +/**
> + * \file lsc_base.h
> + * \brief Base types and definitions for LscImplementation class hierarchy
> + *
> + * Split the common classes and types definitions to a dedicated file to avoid
> + * circular inclusions.
> + *
> + * Both LscGrid and LscPolynomial inherit from LscImplementation and they are
> + * instantiated from lsc.h as the LscAlgorithm::init() function is there inlined.
> + *
> + * For this reason lsc.h needs to include lsc_grid.h and lsc_polynomial.h which
> + * need the LscImplementation class definition themselves.
> + *
> + * Split the LscImplementation interface definition to this file so that it can
> + * be included by both lsc_grid.h and lsc_polynomial.h, which are then included
> + * by lsc.h.
> + */
> +
> +namespace libcamera {
> +
> +namespace ipa {
> +
> +namespace lsc {
> +
> +/**
> + * \class Components
> + * \brief Associate a colour components with a list of gains
> + * \tparam T The type used to store the gain values when loaded from tuning file
> + *
> + * Lsc tables are defined as a list of gain values associated to a colour
> + * component.
> + *
> + * As different ISP support different colour components (usually 'r', 'gr',
> + * 'gb', 'b' or just 'r', 'g', 'b') this class associates a string
> + * identifier for the colour component to a list of gains of type \a T.
> + *
> + * Each key name shall match an entry in the tuning file and the type \a T
> + * shall match the size of the registers where gains are stored.
> + *
> + * The list of keys is provided to the LscAlgorithm class using \a
> + * LscDescriptor::keys.
> + */
> +
> +/**
> + * \class ComponentsMap
> + * \brief Associate a colour temperature to a lsc table
> + * \tparam T The type used to store the gain values when loaded from tuning file
> + *
> + * An lsc table is generated during the tuning phase for a specific light
> + * temperature, and a tuning file usually contains lsc tables generated for
> + * several different colour temperatures.
> + *
> + * This class associates an lsc table to the colour temperature used when the
> + * table has been generated.
> + */
> +
> +} /* namespace lsc */
> +
> +/**
> + * \struct LscDescriptor
> + * \brief Describe the ISP lsc engine
> + *
> + * \var LscDescriptor::keys
> + * \brief The list of colour components to which a list of gains is associated
> + * with in the tuning file. Used for parsing the tuning file
> + *
> + * \var LscDescriptor::numHCells
> + * \brief Number of horizontal cells of the ISP lsc grid. Used for validating
> + * the list of gains parsed from tuning file
> + *
> + * \var LscDescriptor::numVCells
> + * \brief Number of vertical cells of the ISP lsc grid. Used for validating
> + * the list of gains parsed from tuning file
> + *
> + * \var LscDescriptor::sensorSize
> + * \brief The physical sensor size. This is the largest frame size used to
> + * generate the lsc table. Only used by the polynomial lsc algorithm
> + */
> +
> +/**
> + * \class LscImplementation
> + * \brief Pure virtual base class for lsc algorithm implementations
> + * \tparam T The type used to store the gain values when loaded from tuning file
> + * \tparam U The fixedpoint format used to convert gain values generated by
> + * polynomial expansion to the register format
> + *
> + * Defines the interface for the lsc algorithm implementation. Currently
> + * implemented by LscGrid and LscPolynomial.
> + */
> +
> +/**
> + * \fn LscImplementation::~LscImplementation
> + * \brief Virtual class destructor
> + */
> +
> +/**
> + * \fn LscImplementation::parseLscData
> + * \brief Parse \a tuningData using \a descriptor
> + * \param[in] tuningData The tuning data
> + * \param[in] descriptor The lsc engine descriptor
> + *
> + * Parse the tuning file using the \a descriptor to identify the colour
> + * components in the tuning data and validate the size of the loaded gains
> + * tables.
> + *
> + * \return 0 on success, a negative error number otherwise
> + */
> +
> +/**
> + * \fn LscImplementation::resampleLscData
> + * \brief Re-sample the lsc components for \a cropRectangle
> + * \param[in] cropRectangle The sensor analogue crop rectangle
> + * \param[in] xPos List of horizontal positions of the lsc grid nodes
> + * \param[in] yPos List of vertical positions of the lsc grid nodes
> + *
> + * Lsc tables are expressed in two formats:
> + * - A list of gain values (LscGrid)
> + * - A radial polynomial (LscPolynomial)
> + *
> + * Grid-based lsc data are generated using an image at a fixed resolution and
> + * can't at the moment be re-sampled when a different resolution is used for a
> + * streaming session. Re-sampling a grid lsc table will return the same table
> + * as loaded from the tuning file.
> + *
> + * Polynomial are more flexible and can be re-sampled for a given resolution
> + * using a list of horizontal and vertical nodes that define the lsc grid.


> + * Polynomial lsc tables have to be re-sampled every time a new configuration is
> + * applied, as each streaming session might use a different sensor crop
> + * rectangle.


Technically - that applies to Grid too - it's just that we're not
doing it! It might be easy to remove the word 'Polynomial' from the
beginning of that paragraph.


> + *
> + * \a cropRectangle represents the size of the frame on which the polynomial Lsc
> + * has to be re-sampled on.

And this shouldn't say polynomial either in that case.

> + *
> + * \a xPos and \a yPos represent the position of the grid nodes vertexes in
> + * the [0, 1] interval. In example an equally spaced grid of 16 nodes will have
> + * each segment of size 0.0625 and the list of nodes position will be
> + * [0, 0.0625, 0.125, 0.1875, ... , 1]. It is expected that the first position
> + * is 0 and the last position is 1.
> + */
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
> new file mode 100644
> index 000000000000..aecae305aa9c
> --- /dev/null
> +++ b/src/ipa/libipa/lsc_base.h
> @@ -0,0 +1,59 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026 Ideas on Board Oy
> + *
> + * Base classes and types for LSC algorithms implementations
> + */
> +
> +#pragma once
> +
> +#include <map>
> +#include <string>
> +#include <vector>
> +
> +#include <libcamera/geometry.h>
> +
> +#include "libcamera/internal/value_node.h"
> +
> +namespace libcamera {
> +
> +namespace ipa {
> +
> +namespace lsc {
> +
> +template<typename T>
> +class Components : public std::map<std::string, std::vector<T>>
> +{
> +};
> +
> +template<typename T>
> +class ComponentsMap : public std::map<unsigned int, Components<T>>
> +{
> +};
> +
> +} /* namespace lsc */
> +
> +struct LscDescriptor {
> +       std::vector<std::string> keys;
> +       unsigned int numHCells;
> +       unsigned int numVCells;
> +       Size sensorSize;
> +};
> +
> +template<typename T, typename U>
> +class LscImplementation
> +{
> +public:
> +       virtual ~LscImplementation() = default;
> +
> +       virtual int parseLscData(const ValueNode &tuningData,
> +                                const LscDescriptor &descriptor) = 0;
> +
> +       virtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> +                                                     const std::vector<double> &xPos,
> +                                                     const std::vector<double> &yPos) = 0;
> +};
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_grid.cpp b/src/ipa/libipa/lsc_grid.cpp
> new file mode 100644
> index 000000000000..878994acc8c9
> --- /dev/null
> +++ b/src/ipa/libipa/lsc_grid.cpp
> @@ -0,0 +1,33 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026, Ideas On Board
> + *
> + * Grid based lens shading correction
> + */
> +
> +#include "lsc_grid.h"
> +
> +/**
> + * \file lsc_grid.h
> + * \brief LscGrid class
> + */
> +
> +namespace libcamera {
> +
> +LOG_DEFINE_CATEGORY(LscGrid)
> +
> +namespace ipa {
> +
> +/**
> + * \class LscGrid
> + * \brief Grid based lsc algorithm implementation
> + *
> + * Grid based lsc algorithm implementation. The LscGrid class implements lsc
> + * support using tabular lsc data.
> + *
> + * \sa LscImplementation
> + * \sa LscAlgorithm
> + */
> +
> +} /* namespace ipa */
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_grid.h b/src/ipa/libipa/lsc_grid.h
> new file mode 100644
> index 000000000000..e92cde95d18d
> --- /dev/null
> +++ b/src/ipa/libipa/lsc_grid.h
> @@ -0,0 +1,113 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026, Ideas On Board
> + *
> + * Grid based lens shading correction
> + */
> +
> +#pragma once
> +
> +#include <vector>
> +
> +#include <libcamera/base/log.h>
> +
> +#include "lsc_base.h"
> +
> +namespace libcamera {
> +
> +LOG_DECLARE_CATEGORY(LscGrid)
> +
> +namespace ipa {
> +
> +template<typename T, typename U>
> +class LscGrid : public LscImplementation<T, U>
> +{
> +public:
> +       ~LscGrid() {}
> +
> +       int parseLscData(const ValueNode &yamlSets,
> +                        const LscDescriptor &descriptor) override
> +       {
> +               const auto &sets = yamlSets.asList();
> +
> +               for (const auto &yamlSet : sets) {
> +                       uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> +
> +                       int ret = parseLscComponent(yamlSet, ct, descriptor);
> +                       if (ret)
> +                               return ret;
> +               }
> +
> +               if (lscData_.empty()) {
> +                       LOG(LscGrid, Error) << "Failed to load any sets";
> +                       return -EINVAL;
> +               }
> +
> +               return 0;
> +       }
> +
> +       lsc::ComponentsMap<T> resampleLscData([[maybe_unused]] const Rectangle &cropRectangle,
> +                                             [[maybe_unused]] const std::vector<double> &xPos,
> +                                             [[maybe_unused]] const std::vector<double> &yPos) override
> +       {
> +               /* No resampling for grid-based LSC algorithm. */

I wonder if we should record the rectangle of the original grid data
somewhere in the tuning file/process.

Then we could report an error message here saying what the Rectangle is
of the grid, and what was asked. Or of course we could resample it.

Can be on top though - we know we're not resampling grids yet.


As best as I can tell, most of the code below is refactoring for the new
layout, so there's more churn and movement in there than anything new,
and ultimately it leaves the rkisp1 with the new shorter implementation
so with the comments above considered how you wish:


Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>


> +               return lscData_;
> +       }
> +
> +private:
> +       int parseLscComponent(const ValueNode &yamlSet,
> +                             unsigned int ct, const LscDescriptor &descriptor)
> +       {
> +               lsc::Components<T> component;
> +               for (auto &k : descriptor.keys) {
> +                       auto [it, inserted] = component.emplace(
> +                               std::piecewise_construct,
> +                               std::forward_as_tuple(k.c_str()),
> +                               std::forward_as_tuple(parseTable(yamlSet,
> +                                                                k.c_str(),
> +                                                                descriptor.numHCells,
> +                                                                descriptor.numVCells)));
> +                       if (!inserted || it->second.empty()) {
> +                               LOG(LscGrid, Error)
> +                                       << "Set " << k << " for color temperature "
> +                                       << ct << " is missing";
> +                               return -EINVAL;
> +                       }
> +               }
> +
> +               auto [it, inserted] = lscData_.emplace(ct, component);
> +               if (!inserted) {
> +                       LOG(LscGrid, Error)
> +                               << "Multiple sets found for color temperature "
> +                               << ct;
> +                       return -EINVAL;
> +               }
> +
> +               return 0;
> +       }
> +
> +       std::vector<T> parseTable(const ValueNode &tuningData,
> +                                 const char *prop, unsigned int numHCells,
> +                                 unsigned int numVCells)
> +       {
> +               unsigned int kLscNumSamples = numHCells * numVCells;
> +
> +               std::vector<T> table =
> +                       tuningData[prop].get<std::vector<T>>().value_or(utils::defopt);
> +               if (table.size() != kLscNumSamples) {
> +                       LOG(LscGrid, Error)
> +                               << "Invalid '" << prop << "' values: expected "
> +                               << kLscNumSamples
> +                               << " elements, got " << table.size();
> +                       return {};
> +               }
> +
> +               return table;
> +       }
> +
> +       lsc::ComponentsMap<T> lscData_;
> +};
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
> index f607d86c54c3..8d3f1883d6a5 100644
> --- a/src/ipa/libipa/lsc_polynomial.cpp
> +++ b/src/ipa/libipa/lsc_polynomial.cpp
> @@ -1,12 +1,14 @@
>  /* SPDX-License-Identifier: LGPL-2.1-or-later */
>  /*
> - * Copyright (C) 2024, Ideas On Board
> + * Copyright (C) 2026, Ideas On Board
>   *
> - * Polynomial class to represent lens shading correction
> + * Polynomial based lens shading correction
>   */
>  
>  #include "lsc_polynomial.h"
>  
> +#include <assert.h>
> +
>  #include <libcamera/base/log.h>
>  
>  /**
> @@ -21,7 +23,7 @@ LOG_DEFINE_CATEGORY(LscPolynomial)
>  namespace ipa {
>  
>  /**
> - * \class LscPolynomial
> + * \class Polynomial
>   * \brief Class for handling even polynomials used in lens shading correction
>   *
>   * Shading artifacts of camera lenses can be modeled using even radial
> @@ -31,7 +33,7 @@ namespace ipa {
>   */
>  
>  /**
> - * \fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> + * \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
>                       double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
>                       double k4 = 0.0)
>   * \brief Construct a polynomial using the given coefficients
> @@ -45,7 +47,13 @@ namespace ipa {
>   */
>  
>  /**
> - * \fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)
> + * \fn Polynomial::Polynomial(const Polynomial &other)
> + * \brief Construct a Polynomial by copy
> + * \param[in] other The Polynomial to copy construct from
> + */
> +
> +/**
> + * \fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)
>   * \brief Sample the polynomial at the given normalized pixel position
>   *
>   * This functions samples the polynomial at the given pixel position divided by
> @@ -55,9 +63,21 @@ namespace ipa {
>   * \param y y position in normalized coordinates
>   * \return The sampled value
>   */
> +double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
> +{
> +       double dx = x - cnx_;
> +       double dy = y - cny_;
> +       double r = sqrt(dx * dx + dy * dy);
> +       double res = 1.0;
> +
> +       for (unsigned int i = 0; i < coefficients_.size(); i++)
> +               res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> +
> +       return res;
> +}
>  
>  /**
> - * \fn LscPolynomial::getM()
> + * \fn Polynomial::getM()
>   * \brief Get the value m as described in the dng specification
>   *
>   * Returns m according to dng spec. m represents the Euclidean distance
> @@ -66,9 +86,18 @@ namespace ipa {
>   *
>   * \return The sampled value
>   */
> +double Polynomial::getM() const
> +{
> +       double cpx = imageSize_.width * cx_;
> +       double cpy = imageSize_.height * cy_;
> +       double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
> +       double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
> +
> +       return sqrt(mx * mx + my * my);
> +}
>  
>  /**
> - * \fn LscPolynomial::setReferenceImageSize(const Size &size)
> + * \fn Polynomial::setReferenceImageSize(const Size &size)
>   * \brief Set the reference image size
>   *
>   * Set the reference image size that is used for subsequent calls to getM() and
> @@ -76,6 +105,92 @@ namespace ipa {
>   *
>   * \param size The size of the reference image
>   */
> +void Polynomial::setReferenceImageSize(const Size &size)
> +{
> +       assert(!size.isNull());
> +       imageSize_ = size;
> +
> +       /* Calculate normalized centers */
> +       double m = getM();
> +       cnx_ = (size.width * cx_) / m;
> +       cny_ = (size.height * cy_) / m;
> +}
> +
> +/**
> + * \class LscPolynomialBase
> + * \brief Base class for LscPolynomial
> + *
> + * Base class for LscPolynomial for non-templated functions.
> + */
> +
> +/**
> + * \brief Parse polynomial lsc data
> + * \param[in] yamlSets The tuning file content
> + * \param[in] descriptor The lsc engine descriptor
> + *
> + * Parse the lsc data in polyomial form from the \a yamlSet tuning data.
> + */
> +int LscPolynomialBase::parseLscData(const ValueNode &yamlSets,
> +                                   const LscDescriptor &descriptor)
> +{
> +       const auto &sets = yamlSets.asList();
> +       for (const auto &yamlSet : sets) {
> +               uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> +
> +               Components components;
> +               for (auto &k : descriptor.keys) {
> +                       auto polynomial = yamlSet[k.c_str()].get<Polynomial>();
> +                       if (!polynomial) {
> +                               LOG(LscPolynomial, Error)
> +                                       << "Missing polynomial for component "
> +                                       << k;
> +                               return -EINVAL;
> +                       }
> +
> +                       auto [it, inserted] =
> +                               components.emplace(std::piecewise_construct,
> +                                                  std::forward_as_tuple(k.c_str()),
> +                                                  std::forward_as_tuple(*polynomial));
> +
> +                       it->second.setReferenceImageSize(descriptor.sensorSize);
> +               }
> +
> +               auto [it, inserted] = lscData_.emplace(ct, components);
> +               if (!inserted) {
> +                       LOG(LscPolynomial, Error)
> +                               << "Multiple sets found for "
> +                               << "color temperature " << ct;
> +                       return -EINVAL;
> +               }
> +       }
> +
> +       if (lscData_.empty()) {
> +               LOG(LscPolynomial, Error) << "Failed to load any sets";
> +               return -EINVAL;
> +       }
> +
> +       return 0;
> +}
> +
> +/**
> + * \var LscPolynomialBase::lscData_
> + * \brief The polynomial lsc data
> + *
> + * Maps colour temperatures to per-colour radial polynomial definitions.
> + */
> +
> +/**
> + * \class LscPolynomial
> + * \brief Radial Polynomial lsc algorithm implementation
> + *
> + * Polynomial-based lsc algorithm implementation. The LscPolynomial class
> + * implements lsc support using Polynomial to represent the shading artifacts
> + * map.
> + *
> + * \sa LscImplementation
> + * \sa LscAlgorithm
> + *
> + */
>  
> -} // namespace ipa
> -} // namespace libcamera
> +} /* namespace ipa */
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_polynomial.h b/src/ipa/libipa/lsc_polynomial.h
> index d7d9ae42e360..72cca591f260 100644
> --- a/src/ipa/libipa/lsc_polynomial.h
> +++ b/src/ipa/libipa/lsc_polynomial.h
> @@ -1,15 +1,17 @@
>  /* SPDX-License-Identifier: LGPL-2.1-or-later */
>  /*
> - * Copyright (C) 2024, Ideas On Board
> + * Copyright (C) 2026, Ideas On Board
>   *
> - * Helper for radial polynomial used in lens shading correction.
> + * Polynomial based lens shading correction
>   */
> +
>  #pragma once
>  
>  #include <algorithm>
>  #include <array>
> -#include <assert.h>
>  #include <cmath>
> +#include <optional>
> +#include <tuple>
>  
>  #include <libcamera/base/log.h>
>  #include <libcamera/base/span.h>
> @@ -18,64 +20,117 @@
>  
>  #include "libcamera/internal/value_node.h"
>  
> +#include "lsc_base.h"
> +
>  namespace libcamera {
>  
>  LOG_DECLARE_CATEGORY(LscPolynomial)
>  
>  namespace ipa {
>  
> -class LscPolynomial
> +class Polynomial
>  {
>  public:
> -       LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> -                     double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> -                     double k4 = 0.0)
> +       Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> +                  double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> +                  double k4 = 0.0)
>                 : cx_(cx), cy_(cy), cnx_(0), cny_(0),
>                   coefficients_({ k0, k1, k2, k3, k4 })
>         {
>         }
>  
> -       double sampleAtNormalizedPixelPos(double x, double y) const
> +       Polynomial(const Polynomial &other) = default;
> +
> +       double sampleAtNormalizedPixelPos(double x, double y) const;
> +       double getM() const;
> +       void setReferenceImageSize(const Size &size);
> +
> +private:
> +       double cx_;
> +       double cy_;
> +       double cnx_;
> +       double cny_;
> +       std::array<double, 5> coefficients_;
> +       Size imageSize_;
> +};
> +
> +class LscPolynomialBase
> +{
> +private:
> +       using Components = std::map<std::string, Polynomial>;
> +       using ComponentsMap = std::map<unsigned int, Components>;
> +
> +public:
> +       int parseLscData(const ValueNode &yamlSets,
> +                        const LscDescriptor &descriptor);
> +
> +protected:
> +       ComponentsMap lscData_;
> +};
> +
> +template<typename T, typename U>
> +class LscPolynomial : public LscPolynomialBase, public LscImplementation<T, U>
> +{
> +public:
> +       ~LscPolynomial() {}
> +
> +       int parseLscData(const ValueNode &yamlSets,
> +                        const LscDescriptor &descriptor) override
>         {
> -               double dx = x - cnx_;
> -               double dy = y - cny_;
> -               double r = sqrt(dx * dx + dy * dy);
> -               double res = 1.0;
> -               for (unsigned int i = 0; i < coefficients_.size(); i++) {
> -                       res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> -               }
> -               return res;
> +               return LscPolynomialBase::parseLscData(yamlSets, descriptor);
>         }
>  
> -       double getM() const
> +       lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> +                                             const std::vector<double> &xPos,
> +                                             const std::vector<double> &yPos) override
>         {
> -               double cpx = imageSize_.width * cx_;
> -               double cpy = imageSize_.height * cy_;
> -               double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
> -               double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
> +               lsc::ComponentsMap<T> components;
>  
> -               return sqrt(mx * mx + my * my);
> -       }
> +               for (auto &[t, c] : lscData_) {
> +                       lsc::Components<T> comp;
>  
> -       void setReferenceImageSize(const Size &size)
> -       {
> -               assert(!size.isNull());
> -               imageSize_ = size;
> +                       for (auto &[k, p] : c) {
> +                               comp.emplace(std::piecewise_construct,
> +                                            std::forward_as_tuple(k),
> +                                            std::forward_as_tuple(samplePolynomial(p, xPos, yPos,
> +                                                                                   cropRectangle)));
> +                       }
>  
> -               /* Calculate normalized centers */
> -               double m = getM();
> -               cnx_ = (size.width * cx_) / m;
> -               cny_ = (size.height * cy_) / m;
> +                       components[t] = comp;
> +               }
> +
> +               return components;
>         }
>  
>  private:
> -       double cx_;
> -       double cy_;
> -       double cnx_;
> -       double cny_;
> -       std::array<double, 5> coefficients_;
> +       std::vector<T> samplePolynomial(const Polynomial &poly,
> +                                       Span<const double> xPositions,
> +                                       Span<const double> yPositions,
> +                                       const Rectangle &cropRectangle)
>  
> -       Size imageSize_;
> +       {
> +               double m = poly.getM();
> +               double x0 = cropRectangle.x / m;
> +               double y0 = cropRectangle.y / m;
> +               double w = cropRectangle.width / m;
> +               double h = cropRectangle.height / m;
> +               std::vector<T> samples;
> +
> +               samples.reserve(xPositions.size() * yPositions.size());
> +
> +               for (double y : yPositions) {
> +                       for (double x : xPositions) {
> +                               double xp = x0 + x * w;
> +                               double yp = y0 + y * h;
> +                               float sample = static_cast<float>
> +                                               (poly.sampleAtNormalizedPixelPos(xp, yp));
> +
> +                               samples.push_back(U(sample).quantized());
> +                       }
> +               }
> +
> +               return samples;
> +       }
>  };
>  
>  } /* namespace ipa */
> @@ -83,8 +138,8 @@ private:
>  #ifndef __DOXYGEN__
>  
>  template<>
> -struct ValueNode::Accessor<ipa::LscPolynomial> {
> -       std::optional<ipa::LscPolynomial> get(const ValueNode &obj) const
> +struct ValueNode::Accessor<ipa::Polynomial> {
> +       std::optional<ipa::Polynomial> get(const ValueNode &obj) const
>         {
>                 std::optional<double> cx = obj["cx"].get<double>();
>                 std::optional<double> cy = obj["cy"].get<double>();
> @@ -98,7 +153,7 @@ struct ValueNode::Accessor<ipa::LscPolynomial> {
>                         LOG(LscPolynomial, Error)
>                                 << "Polynomial is missing a parameter";
>  
> -               return ipa::LscPolynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);
> +               return ipa::Polynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);
>         }
>  };
>  
> diff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build
> index edf8eabd8b78..da6ea0c5e130 100644
> --- a/src/ipa/libipa/meson.build
> +++ b/src/ipa/libipa/meson.build
> @@ -14,6 +14,9 @@ libipa_headers = files([
>      'fixedpoint.h',
>      'histogram.h',
>      'interpolator.h',
> +    'lsc.h',
> +    'lsc_base.h',
> +    'lsc_grid.h',
>      'lsc_polynomial.h',
>      'lux.h',
>      'module.h',
> @@ -36,6 +39,8 @@ libipa_sources = files([
>      'fixedpoint.cpp',
>      'histogram.cpp',
>      'interpolator.cpp',
> +    'lsc.cpp',
> +    'lsc_grid.cpp',
>      'lsc_polynomial.cpp',
>      'lux.cpp',
>      'module.cpp',
> diff --git a/src/ipa/rkisp1/algorithms/lsc.cpp b/src/ipa/rkisp1/algorithms/lsc.cpp
> index 25f7c512b35c..51076737ffa5 100644
> --- a/src/ipa/rkisp1/algorithms/lsc.cpp
> +++ b/src/ipa/rkisp1/algorithms/lsc.cpp
> @@ -2,7 +2,7 @@
>  /*
>   * Copyright (C) 2021-2022, Ideas On Board
>   *
> - * RkISP1 Lens Shading Correction control
> + * RkISP1 Lens Shading Correction algorithm
>   */
>  
>  #include "lsc.h"
> @@ -14,56 +14,17 @@
>  #include <libcamera/base/log.h>
>  #include <libcamera/base/utils.h>
>  
> -#include "libcamera/internal/value_node.h"
> -
> -#include "libipa/lsc_polynomial.h"
> -#include "linux/rkisp1-config.h"
> -
>  /**
>   * \file lsc.h
>   */
>  
>  namespace libcamera {
>  
> -namespace ipa {
> -
> -template<typename T>
> -void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,
> -                      std::vector<T> &dest, double lambda)
> -{
> -       ASSERT(a.size() == b.size());
> -       dest.resize(a.size());
> -       for (size_t i = 0; i < a.size(); i++)
> -               dest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;
> -}
> -
> -template<>
> -void Interpolator<rkisp1::algorithms::LensShadingCorrection::Components>::
> -       interpolate(const rkisp1::algorithms::LensShadingCorrection::Components &a,
> -                   const rkisp1::algorithms::LensShadingCorrection::Components &b,
> -                   rkisp1::algorithms::LensShadingCorrection::Components &dest,
> -                   double lambda)
> -{
> -       interpolateVector(a.r, b.r, dest.r, lambda);
> -       interpolateVector(a.gr, b.gr, dest.gr, lambda);
> -       interpolateVector(a.gb, b.gb, dest.gb, lambda);
> -       interpolateVector(a.b, b.b, dest.b, lambda);
> -}
> -
> -} /* namespace ipa */
> -
>  namespace ipa::rkisp1::algorithms {
>  
>  /**
>   * \class LensShadingCorrection
>   * \brief RkISP1 Lens Shading Correction control
> - *
> - * Due to the optical characteristics of the lens, the light intensity received
> - * by the sensor is not uniform.
> - *
> - * The Lens Shading Correction algorithm applies multipliers to all pixels
> - * to compensate for the lens shading effect. The coefficients are
> - * specified in a downscaled table in the YAML tuning file.
>   */
>  
>  LOG_DEFINE_CATEGORY(RkISP1Lsc)
> @@ -72,265 +33,20 @@ namespace {
>  
>  constexpr int kColourTemperatureQuantization = 10;
>  
> -class LscPolynomialShadingDescriptor : public LensShadingCorrection::ShadingDescriptor
> -{
> -public:
> -       LscPolynomialShadingDescriptor(const LscPolynomial &pr, const LscPolynomial &pgr,
> -                                      const LscPolynomial &pgb, const LscPolynomial &pb)
> -               : pr_(pr), pgr_(pgr), pgb_(pgb), pb_(pb)
> -       {
> -       }
> -
> -       LensShadingCorrection::Components sampleForCrop(const Rectangle &cropRectangle,
> -                                                       Span<const double> xSizes,
> -                                                       Span<const double> ySizes) override;
> -
> -private:
> -       std::vector<uint16_t> samplePolynomial(const LscPolynomial &poly,
> -                                              Span<const double> xPositions,
> -                                              Span<const double> yPositions,
> -                                              const Rectangle &cropRectangle);
> -
> -       std::vector<double> sizesListToPositions(Span<const double> sizes);
> -
> -       LscPolynomial pr_;
> -       LscPolynomial pgr_;
> -       LscPolynomial pgb_;
> -       LscPolynomial pb_;
> -};
> -
> -LensShadingCorrection::Components
> -LscPolynomialShadingDescriptor::sampleForCrop(const Rectangle &cropRectangle,
> -                                             Span<const double> xSizes,
> -                                             Span<const double> ySizes)
> -{
> -       std::vector<double> xPos = sizesListToPositions(xSizes);
> -       std::vector<double> yPos = sizesListToPositions(ySizes);
> -
> -       return {
> -               .r = samplePolynomial(pr_, xPos, yPos, cropRectangle),
> -               .gr = samplePolynomial(pgr_, xPos, yPos, cropRectangle),
> -               .gb = samplePolynomial(pgb_, xPos, yPos, cropRectangle),
> -               .b = samplePolynomial(pb_, xPos, yPos, cropRectangle)
> -       };
> -}
> -
> -std::vector<uint16_t>
> -LscPolynomialShadingDescriptor::samplePolynomial(const LscPolynomial &poly,
> -                                                Span<const double> xPositions,
> -                                                Span<const double> yPositions,
> -                                                const Rectangle &cropRectangle)
> -{
> -       double m = poly.getM();
> -       double x0 = cropRectangle.x / m;
> -       double y0 = cropRectangle.y / m;
> -       double w = cropRectangle.width / m;
> -       double h = cropRectangle.height / m;
> -       std::vector<uint16_t> samples;
> -
> -       samples.reserve(xPositions.size() * yPositions.size());
> -
> -       for (double y : yPositions) {
> -               for (double x : xPositions) {
> -                       double xp = x0 + x * w;
> -                       double yp = y0 + y * h;
> -                       /*
> -                        * The hardware uses 2.10 fixed point format and limits
> -                        * the legal values to [1..3.999]. Scale and clamp the
> -                        * sampled value accordingly.
> -                        */
> -                       int v = static_cast<int>(
> -                               poly.sampleAtNormalizedPixelPos(xp, yp) *
> -                               1024);
> -                       v = std::clamp(v, 1024, 4095);
> -                       samples.push_back(v);
> -               }
> -       }
> -       return samples;
> -}
> -
> -/*
> - * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left
> - * quadrant of the grid. This is then mirrored in hardware to the other
> - * quadrants. See parseSizes() for further details. For easier handling, this
> - * function converts the cell sizes of half the grid to a list of position of
> - * the whole grid (on one axis). Example:
> - *
> - * input:   | 0.2 | 0.3 |
> - * output: 0.0   0.2   0.5   0.8   1.0
> - */
> -std::vector<double>
> -LscPolynomialShadingDescriptor::sizesListToPositions(Span<const double> sizes)
> -{
> -       const int half = sizes.size();
> -       std::vector<double> positions(half * 2 + 1);
> -       double x = 0.0;
> -
> -       positions[half] = 0.5;
> -       for (int i = 1; i <= half; i++) {
> -               x += sizes[half - i];
> -               positions[half - i] = 0.5 - x;
> -               positions[half + i] = 0.5 + x;
> -       }
> -
> -       return positions;
> -}
> -
> -class LscPolynomialLoader
> -{
> -public:
> -       LscPolynomialLoader(const Size &sensorSize)
> -               : sensorSize_(sensorSize)
> -       {
> -       }
> -
> -       int parseLscData(const ValueNode &yamlSets,
> -                        LensShadingCorrection::ShadingDescriptorMap &lscData);
> -
> -private:
> -       Size sensorSize_;
> -};
> -
> -int LscPolynomialLoader::parseLscData(const ValueNode &yamlSets,
> -                                     LensShadingCorrection::ShadingDescriptorMap &lscData)
> +unsigned int quantize(unsigned int value, unsigned int step)
>  {
> -       const auto &sets = yamlSets.asList();
> -       for (const auto &yamlSet : sets) {
> -               std::optional<LscPolynomial> pr, pgr, pgb, pb;
> -               uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> -
> -               if (lscData.count(ct)) {
> -                       LOG(RkISP1Lsc, Error)
> -                               << "Multiple sets found for "
> -                               << "color temperature " << ct;
> -                       return -EINVAL;
> -               }
> -
> -               pr = yamlSet["r"].get<LscPolynomial>();
> -               pgr = yamlSet["gr"].get<LscPolynomial>();
> -               pgb = yamlSet["gb"].get<LscPolynomial>();
> -               pb = yamlSet["b"].get<LscPolynomial>();
> -
> -               if (!(pr || pgr || pgb || pb)) {
> -                       LOG(RkISP1Lsc, Error)
> -                               << "Failed to parse polynomial for "
> -                               << "colour temperature " << ct;
> -                       return -EINVAL;
> -               }
> -
> -               pr->setReferenceImageSize(sensorSize_);
> -               pgr->setReferenceImageSize(sensorSize_);
> -               pgb->setReferenceImageSize(sensorSize_);
> -               pb->setReferenceImageSize(sensorSize_);
> -
> -               lscData.emplace(
> -                       ct, std::make_unique<LscPolynomialShadingDescriptor>(
> -                                   *pr, *pgr, *pgb, *pb));
> -       }
> -
> -       if (lscData.empty()) {
> -               LOG(RkISP1Lsc, Error) << "Failed to load any sets";
> -               return -EINVAL;
> -       }
> -
> -       return 0;
> +       return std::lround(value / static_cast<double>(step)) * step;
>  }
>  
> -class LscTableShadingDescriptor : public LensShadingCorrection::ShadingDescriptor
> -{
> -public:
> -       LscTableShadingDescriptor(LensShadingCorrection::Components components)
> -               : lscData_(std::move(components))
> -       {
> -       }
> -
> -       LensShadingCorrection::Components
> -       sampleForCrop([[maybe_unused]] const Rectangle &cropRectangle,
> -                     [[maybe_unused]] Span<const double> xSizes,
> -                     [[maybe_unused]] Span<const double> ySizes) override
> -       {
> -               LOG(RkISP1Lsc, Warning)
> -                       << "Tabular LSC data doesn't support resampling";
> -               return lscData_;
> -       }
> -
> -private:
> -       LensShadingCorrection::Components lscData_;
> -};
> -
> -class LscTableLoader
> -{
> -public:
> -       int parseLscData(const ValueNode &yamlSets,
> -                        LensShadingCorrection::ShadingDescriptorMap &lscData);
> -
> -private:
> -       std::vector<uint16_t> parseTable(const ValueNode &tuningData,
> -                                        const char *prop);
> -};
> -
> -int LscTableLoader::parseLscData(const ValueNode &yamlSets,
> -                                LensShadingCorrection::ShadingDescriptorMap &lscData)
> -{
> -       const auto &sets = yamlSets.asList();
> -
> -       for (const auto &yamlSet : sets) {
> -               uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> -
> -               if (lscData.count(ct)) {
> -                       LOG(RkISP1Lsc, Error)
> -                               << "Multiple sets found for color temperature "
> -                               << ct;
> -                       return -EINVAL;
> -               }
> -
> -               LensShadingCorrection::Components set;
> -               set.r = parseTable(yamlSet, "r");
> -               set.gr = parseTable(yamlSet, "gr");
> -               set.gb = parseTable(yamlSet, "gb");
> -               set.b = parseTable(yamlSet, "b");
> -
> -               if (set.r.empty() || set.gr.empty() ||
> -                   set.gb.empty() || set.b.empty()) {
> -                       LOG(RkISP1Lsc, Error)
> -                               << "Set for color temperature " << ct
> -                               << " is missing tables";
> -                       return -EINVAL;
> -               }
> -
> -               lscData.emplace(
> -                       ct, std::make_unique<LscTableShadingDescriptor>(std::move(set)));
> -       }
> -
> -       if (lscData.empty()) {
> -               LOG(RkISP1Lsc, Error) << "Failed to load any sets";
> -               return -EINVAL;
> -       }
> -
> -       return 0;
> -}
> +} /* namespace */
>  
> -std::vector<uint16_t> LscTableLoader::parseTable(const ValueNode &tuningData,
> -                                                const char *prop)
> +LensShadingCorrection::LensShadingCorrection()
> +       : lastAppliedCt_(0), lastAppliedQuantizedCt_(0)
>  {
> -       static constexpr unsigned int kLscNumSamples =
> -               RKISP1_CIF_ISP_LSC_SAMPLES_MAX * RKISP1_CIF_ISP_LSC_SAMPLES_MAX;
> -
> -       std::vector<uint16_t> table =
> -               tuningData[prop].get<std::vector<uint16_t>>().value_or(utils::defopt);
> -       if (table.size() != kLscNumSamples) {
> -               LOG(RkISP1Lsc, Error)
> -                       << "Invalid '" << prop << "' values: expected "
> -                       << kLscNumSamples
> -                       << " elements, got " << table.size();
> -               return {};
> -       }
> -
> -       return table;
>  }
>  
> -std::vector<double> parseSizes(const ValueNode &tuningData,
> -                              const char *prop)
> +std::vector<double> LensShadingCorrection::parseSizes(const ValueNode &tuningData,
> +                                                     const char *prop)
>  {
>         std::vector<double> sizes =
>                 tuningData[prop].get<std::vector<double>>().value_or(utils::defopt);
> @@ -359,22 +75,36 @@ std::vector<double> parseSizes(const ValueNode &tuningData,
>         return sizes;
>  }
>  
> -unsigned int quantize(unsigned int value, unsigned int step)
> +/*
> + * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left
> + * quadrant of the grid. This is then mirrored in hardware to the other
> + * quadrants. See parseSizes() for further details. For easier handling, this
> + * function converts the cell sizes of half the grid to a list of position of
> + * the whole grid (on one axis). Example:
> + *
> + * input:   | 0.2 | 0.3 |
> + * output: 0.0   0.2   0.5   0.8   1.0
> + */
> +std::vector<double> LensShadingCorrection::sizesToPositions(Span<const double> sizes)
>  {
> -       return std::lround(value / static_cast<double>(step)) * step;
> -}
> +       const int half = sizes.size();
> +       std::vector<double> positions(half * 2 + 1);
> +       double x = 0.0;
>  
> -} /* namespace */
> +       positions[half] = 0.5;
> +       for (int i = 1; i <= half; i++) {
> +               x += sizes[half - i];
> +               positions[half - i] = 0.5 - x;
> +               positions[half + i] = 0.5 + x;
> +       }
>  
> -LensShadingCorrection::LensShadingCorrection()
> -       : lastAppliedCt_(0), lastAppliedQuantizedCt_(0)
> -{
> +       return positions;
>  }
>  
>  /**
>   * \copydoc libcamera::ipa::Algorithm::init
>   */
> -int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
> +int LensShadingCorrection::init(IPAContext &context,
>                                 const ValueNode &tuningData)
>  {
>         xSize_ = parseSizes(tuningData, "x-size");
> @@ -383,56 +113,27 @@ int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
>         if (xSize_.empty() || ySize_.empty())
>                 return -EINVAL;
>  
> -       /* Get all defined sets to apply. */
> -       const ValueNode &yamlSets = tuningData["sets"];
> -       if (!yamlSets.isList()) {
> -               LOG(RkISP1Lsc, Error)
> -                       << "'sets' parameter not found in tuning file";
> -               return -EINVAL;
> -       }
> -
> -       ShadingDescriptorMap lscData;
> -       int ret = 0;
> -
> -       std::string type = tuningData["type"].get<std::string>("table");
> -       if (type == "table") {
> -               LOG(RkISP1Lsc, Debug) << "Loading tabular LSC data.";
> -               auto loader = LscTableLoader();
> -               ret = loader.parseLscData(yamlSets, lscData);
> -       } else if (type == "polynomial") {
> -               LOG(RkISP1Lsc, Debug) << "Loading polynomial LSC data.";
> -               /*
> -                * \todo: Most likely the reference frame should be native_size.
> -                * Let's wait how the internal discussions progress.
> -                */
> -               auto loader = LscPolynomialLoader(context.sensorInfo.activeAreaSize);
> -               ret = loader.parseLscData(yamlSets, lscData);
> -       } else {
> -               LOG(RkISP1Lsc, Error) << "Unsupported LSC data type '"
> -                                     << type << "'";
> -               ret = -EINVAL;
> -       }
> -
> -       if (ret)
> -               return ret;
> +       xPos_ = sizesToPositions(xSize_);
> +       yPos_ = sizesToPositions(ySize_);
>  
> -       context.ctrlMap[&controls::LensShadingCorrectionEnable] =
> -               ControlInfo(false, true, true);
> -
> -       shadingDescriptors_ = std::move(lscData);
> -
> -       return 0;
> +       return lscAlgo_.init(tuningData, context.ctrlMap, {
> +                               .keys = { "r", "gr", "gb", "b" },
> +                               .numHCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,
> +                               .numVCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,
> +                               .sensorSize = context.sensorInfo.activeAreaSize
> +                            });
>  }
>  
>  /**
>   * \copydoc libcamera::ipa::Algorithm::configure
>   */
>  int LensShadingCorrection::configure(IPAContext &context,
> -                                    [[maybe_unused]] const IPACameraSensorInfo &configInfo)
> +                                    const IPACameraSensorInfo &configInfo)
>  {
>         const Size &size = context.configuration.sensor.size;
>         Size totalSize{};
>  
> +       /* Calculate gradients. */
>         for (unsigned int i = 0; i < RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE; ++i) {
>                 xSizes_[i] = xSize_[i] * size.width;
>                 ySizes_[i] = ySize_[i] * size.height;
> @@ -455,16 +156,8 @@ int LensShadingCorrection::configure(IPAContext &context,
>                 yGrad_[i] = std::round(32768 / ySizes_[i]);
>         }
>  
> -       LOG(RkISP1Lsc, Debug) << "Sample LSC data for " << configInfo.analogCrop;
> -       std::map<unsigned int, LensShadingCorrection::Components> shadingData;
> -       for (const auto &[t, descriptor] : shadingDescriptors_)
> -               shadingData[t] = descriptor->sampleForCrop(configInfo.analogCrop,
> -                                                          xSize_, ySize_);
> -
> -       sets_.setData(std::move(shadingData));
> -
> -       context.activeState.lsc.enabled = true;
> -       return 0;
> +       return lscAlgo_.configure(context.activeState.lsc, configInfo.analogCrop,
> +                                 xPos_, yPos_);
>  }
>  
>  void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)
> @@ -476,12 +169,16 @@ void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)
>  }
>  
>  void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
> -                                     const Components &set)
> +                                     const ipa::lsc::Components<uint16_t> &set)
>  {
> -       std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
> -       std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
> -       std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
> -       std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
> +       const auto &r = set.at("r");
> +       std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
> +       const auto &gr = set.at("gr");
> +       std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
> +       const auto &gb = set.at("gb");
> +       std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
> +       const auto &b = set.at("b");
> +       std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
>  }
>  
>  /**
> @@ -492,19 +189,8 @@ void LensShadingCorrection::queueRequest(IPAContext &context,
>                                          IPAFrameContext &frameContext,
>                                          const ControlList &controls)
>  {
> -       auto &lsc = context.activeState.lsc;
> -
> -       const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);
> -       if (lscEnable && *lscEnable != lsc.enabled) {
> -               lsc.enabled = *lscEnable;
> -
> -               LOG(RkISP1Lsc, Debug)
> -                       << (lsc.enabled ? "Enabling" : "Disabling") << " Lsc";
> -
> -               frameContext.lsc.update = true;
> -       }
> -
> -       frameContext.lsc.enabled = lsc.enabled;
> +       lscAlgo_.queueRequest(context.activeState.lsc, frameContext.lsc,
> +                             controls);
>  }
>  
>  /**
> @@ -542,7 +228,7 @@ void LensShadingCorrection::prepare([[maybe_unused]] IPAContext &context,
>  
>         setParameters(*config);
>  
> -       const Components &set = sets_.getInterpolated(quantizedCt);
> +       const auto &set = lscAlgo_.interpolateComponents(quantizedCt);
>         copyTable(*config, set);
>  
>         lastAppliedCt_ = ct;
> @@ -562,7 +248,7 @@ void LensShadingCorrection::process([[maybe_unused]] IPAContext &context,
>                                     [[maybe_unused]] const rkisp1_stat_buffer *stats,
>                                     ControlList &metadata)
>  {
> -       metadata.set(controls::LensShadingCorrectionEnable, frameContext.lsc.enabled);
> +       lscAlgo_.process(frameContext.lsc, metadata);
>  }
>  
>  REGISTER_IPA_ALGORITHM(LensShadingCorrection, "LensShadingCorrection")
> diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
> index 0a256e225327..7c656d95acfb 100644
> --- a/src/ipa/rkisp1/algorithms/lsc.h
> +++ b/src/ipa/rkisp1/algorithms/lsc.h
> @@ -2,17 +2,23 @@
>  /*
>   * Copyright (C) 2021-2022, Ideas On Board
>   *
> - * RkISP1 Lens Shading Correction control
> + * RkISP1 Lens Shading Correction algorithm
>   */
>  
>  #pragma once
>  
> -#include <map>
> -#include <memory>
> +#include <vector>
>  
> -#include "libipa/interpolator.h"
> +#include <linux/rkisp1-config.h>
> +
> +#include "libcamera/internal/value_node.h"
> +
> +#include "libipa/fixedpoint.h"
> +#include "libipa/lsc.h"
>  
>  #include "algorithm.h"
> +#include "ipa_context.h"
> +#include "params.h"
>  
>  namespace libcamera {
>  
> @@ -37,39 +43,28 @@ public:
>                      const rkisp1_stat_buffer *stats,
>                      ControlList &metadata) override;
>  
> -       struct Components {
> -               std::vector<uint16_t> r;
> -               std::vector<uint16_t> gr;
> -               std::vector<uint16_t> gb;
> -               std::vector<uint16_t> b;
> -       };
> -
> -       class ShadingDescriptor
> -       {
> -       public:
> -               virtual ~ShadingDescriptor() = default;
> -               virtual Components sampleForCrop(const Rectangle &cropRectangle,
> -                                                Span<const double> xSizes,
> -                                                Span<const double> ySizes) = 0;
> -       };
> -
> -       using ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;
> -
>  private:
> +       std::vector<double> parseSizes(const ValueNode &tuningData,
> +                                      const char *prop);
> +       std::vector<double> sizesToPositions(Span<const double> sizes);
> +
>         void setParameters(rkisp1_cif_isp_lsc_config &config);
> -       void copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);
> +       void copyTable(rkisp1_cif_isp_lsc_config &config,
> +                      const ipa::lsc::Components<uint16_t> &set0);
>  
> -       ShadingDescriptorMap shadingDescriptors_;
> -       ipa::Interpolator<Components> sets_;
>         std::vector<double> xSize_;
>         std::vector<double> ySize_;
>         uint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
>         uint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
>         uint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
>         uint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> +       std::vector<double> xPos_;
> +       std::vector<double> yPos_;
>  
>         unsigned int lastAppliedCt_;
>         unsigned int lastAppliedQuantizedCt_;
> +
> +       LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;
>  };
>  
>  } /* namespace ipa::rkisp1::algorithms */
> diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
> index cd97e10bcf2b..005f4102b4f6 100644
> --- a/src/ipa/rkisp1/ipa_context.h
> +++ b/src/ipa/rkisp1/ipa_context.h
> @@ -30,6 +30,7 @@
>  #include "libipa/ccm.h"
>  #include "libipa/fc_queue.h"
>  #include "libipa/fixedpoint.h"
> +#include "libipa/lsc.h"
>  
>  namespace libcamera {
>  
> @@ -138,9 +139,7 @@ struct IPAActiveState {
>                 double strength;
>         } wdr;
>  
> -       struct {
> -               bool enabled;
> -       } lsc;
> +       ipa::lsc::ActiveState lsc;
>  };
>  
>  struct IPAFrameContext : public FrameContext {
> @@ -213,10 +212,7 @@ struct IPAFrameContext : public FrameContext {
>                 double gain;
>         } wdr;
>  
> -       struct {
> -               bool enabled;
> -               bool update;
> -       } lsc;
> +       ipa::lsc::FrameContext lsc;
>  };
>  
>  struct IPAContext {
> 
> -- 
> 2.54.0
>
Dan Scally June 23, 2026, 9:30 a.m. UTC | #2
Morning Jacopo

On 15/06/2026 15:05, Jacopo Mondi wrote:
> Introduce an Lsc algorithm implementation in libIPA.
> 
> The LscAlgorithm class supports two algorithms: grid-based lsc and
> polynomial-based lsc.
> 
> Both implementations have been generalized from the existing ones in the
> RkISP1 IPA.
> 
> Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> ---
>   src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++
>   src/ipa/libipa/lsc.h              | 145 +++++++++++++
>   src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++
>   src/ipa/libipa/lsc_base.h         |  59 ++++++
>   src/ipa/libipa/lsc_grid.cpp       |  33 +++
>   src/ipa/libipa/lsc_grid.h         | 113 ++++++++++
>   src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-
>   src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----
>   src/ipa/libipa/meson.build        |   5 +
>   src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------
>   src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--
>   src/ipa/rkisp1/ipa_context.h      |  10 +-
>   12 files changed, 1126 insertions(+), 450 deletions(-)
> 
> diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
> new file mode 100644
> index 000000000000..ae713a776778
> --- /dev/null
> +++ b/src/ipa/libipa/lsc.cpp
> @@ -0,0 +1,322 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026 Ideas on Board Oy
> + *
> + * libIPA Lsc algorithms
> + */
> +
> +#include "lsc.h"
> +
> +#include <libcamera/base/log.h>
> +
> +/**
> + * \file lsc.h
> + * \brief libipa lsc algorithm
> + */
> +
> +namespace libcamera {
> +
> +LOG_DEFINE_CATEGORY(Lsc)
> +
> +namespace ipa {
> +
> +namespace lsc {
> +
> +/**
> + * \struct ActiveState
> + * \brief The lsc active state
> + *
> + * \var ActiveState::enabled
> + * \brief Boolean flag for the LscAlgorithm enable status
> + */
> +
> +/**
> + * \struct FrameContext
> + * \brief The lsc frame context
> + *
> + * \var FrameContext::enabled
> + * \brief Boolean flag for the LscAlgorithm enable status
> + *
> + * \var FrameContext::update
> + * \brief Boolean flag for the LscAlgorithm updated status
> + */
> +
> +} /* namespace lsc */
> +
> +#ifndef __DOXYGEN__
> +template<>
> +void Interpolator<lsc::Components<uint16_t>>::
> +	interpolate(const lsc::Components<uint16_t> &a,
> +		    const lsc::Components<uint16_t> &b,
> +		    lsc::Components<uint16_t> &dest,
> +		    double lambda)
> +{
> +	for (auto const &[k, v] : a)
> +		interpolateVector(v, b.at(k), dest[k], lambda);
> +}
> +#endif
> +
> +/**
> + * \class LscAlgorithmBase
> + * \brief Base class for LscAlgorithm
> + *
> + * Base class for LscAlgorithm for non-templated functions implementation
> + */
> +
> +/**
> + * \brief Queue a request to the lsc algorithm
> + * \param[in] state The lsc active state
> + * \param[in] context The lsc frame context
> + * \param[in] controls The list of controls associated with a Request
> + *
> + * Queue a new list of \a controls to the lsc algorithm.
> + * The only supported control is controls::LensShadingCorrectionEnable.
> + */
> +void LscAlgorithmBase::queueRequest(lsc::ActiveState &state,
> +				    lsc::FrameContext &context,
> +				    const ControlList &controls)
> +{
> +	const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);
> +	if (lscEnable && *lscEnable != state.enabled) {
> +		state.enabled = *lscEnable;
> +
> +		LOG(Lsc, Debug)
> +			<< (state.enabled ? "Enabling" : "Disabling") << " Lsc";
> +
> +		context.update = true;
> +	}
> +
> +	context.enabled = state.enabled;
> +}
> +
> +/**
> + * \brief Populate the list of lsc metadata
> + * \param[in] context The lsc frame context
> + * \param[in] metadata The list of metadata
> + *
> + * Populates the list of \a metadata with controls handled by the LscAlgorithm
> + * class. The only supported metadata is controls::LensShadingCorrectionEnable.
> + */
> +void LscAlgorithmBase::process(lsc::FrameContext &context, ControlList &metadata)
> +{
> +	metadata.set(controls::LensShadingCorrectionEnable, context.enabled);
> +}
> +
> +/**
> + * \class LscAlgorithm
> + * \brief libIPA lsc algorithm implementation
> + * \tparam T The type used to store the gain values when loaded from tuning file
> + * \tparam U The fixedpoint lsc engine register format
> + *
> + * Due to the optical characteristics of the lens, the light intensity received
> + * by the sensor is not uniform. The Lens Shading Correction algorithm applies
> + * multipliers to all pixels to compensate for the lens shading effect.
> + *
> + * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.
> + * It provides support for parsing the tuning file content and generate tables

s/generate/generates?

> + * of per-colour temperature gains that IPA algorithms can use to program their
> + * lsc engine.
> + *
> + * The init() function parses the tuning file and loads the gain tables either
> + * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain
> + * tables are organized per-colour temperature with per-colour components gain
> + * vectors or polynomial coefficients. The colour components names are
> + * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc
> + * engine support 4 colour components (r, gr, gb, b), some only support 3 colour
> + * components (r, g, b). The name (and number) of the expected colour components
> + * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys
> + * field.
> + *
> + * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour
> + * components. The gain table has been here omitted, but the expected number
> + * of entries has be equal to
> + * LscDescriptor::numHCells * LscDescriptor::numVCells.
> + *
> + * \code{.yaml}
> + * - Lsc:
> + *    sets:
> + *      - ct: 2500
> + *        r: [
> + *        	.. gains table omitted..
> + *        ]
> + *        g: [
> + *        	.. gains table omitted..
> + *        ]
> + *        b: [
> + *        	.. gains table omitted..
> + *        ]
> + *      - ct: 6500
> + *        r: [
> + *        	.. gains table omitted..
> + *        ]
> + *        g: [
> + *        	.. gains table omitted..
> + *        ]
> + *        b: [
> + *        	.. gains table omitted..
> + *        ]
> + * \endcode
> + *
> + * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'
> + * colour components:
> + *
> + * \code{.yaml}
> + * - Lsc:
> + *    type: "polynomial"
> + *    sets:
> + *      - ct: 2500
> + *        r:
> + *          cx: 0.5006571711950275
> + *          cy: 0.510093737499277
> + *          k0: 1.5393282208428813
> + *          k1: -1.1434559757908016
> + *          k2: 4.332602305814554
> + *          k3: 0.0
> + *          k4: 0.0
> + *        gr:
> + *          cx: 0.5009320529087338
> + *          cy: 0.511208038949085
> + *          k0: 1.5634738574805407
> + *          k1: -1.5623484259968348
> + *          k2: 4.846686073656501
> + *          k3: 0.0
> + *          k4: 0.0
> + *        gb:
> + *          cx: 0.5012013290343839
> + *          cy: 0.5128251541578288
> + *          k0: 1.526147944919103
> + *          k1: -1.4316976083689723
> + *          k2: 4.792604063222728
> + *          k3: 0.0
> + *          k4: 0.0
> + *        b:
> + *          cx: 0.49864139511067784
> + *          cy: 0.5162095081739346
> + *          k0: 1.0405245474038738
> + *          k1: 0.05618339879447103
> + *          k2: 1.8792813594001752
> + *          k3: 0.0
> + *          k4: 0.0
> + *      - ct: 6000
> + *        r:
> + *          cx: 0.5006202239353942
> + *          cy: 0.5099531318307661
> + *          k0: 1.4702946023945032
> + *          k1: -0.8893767547927631
> + *          k2: 3.920547732201387
> + *          k3: 0.0
> + *          k4: 0.0
> + *        gr:
> + *          cx: 0.500907874178317
> + *          cy: 0.511084916024106
> + *          k0: 1.5336172760559457
> + *          k1: -1.39964026514435
> + *          k2: 4.565487728954618
> + *          k3: 0.0
> + *          k4: 0.0
> + *        gb:
> + *          cx: 0.5011898608900477
> + *          cy: 0.5126797906745105
> + *          k0: 1.5013145790354843
> + *          k1: -1.2747407173754124
> + *          k2: 4.514682876897286
> + *          k3: 0.0
> + *          k4: 0.0
> + *        b:
> + *          cx: 0.4987561413116136
> + *          cy: 0.5159619420778772
> + *          k0: 1.0102986422191802
> + *          k1: 0.13263449763985727
> + *          k2: 1.686556107316064
> + *          k3: 0.0
> + *          k4: 0.0
> + * \endcode
> + *
> + * The lsc tables or the polynomial definition are generated at tuning time
> + * using an image of known resolution which needs to be specified in
> + * LscDescriptor::sensorSize.
> + *
> + * At LscAlgorithm::configure() time the lsc tables are re-sampled on the
> + * sensor's crop rectangle in use to adapt them to the configuration in use for
> + * a streaming session. Polynomial lsc tables support re-sampling and can be
> + * applied to any sensor configuration. Grid-based lsc tables cannot be
> + * re-sampled and the configuration as parsed from the tuning file is used for
> + * all sensor configurations providing best-effort results.
> + *
> + * When the IPA algorithms wants to get access to the (resampled) tables to
> + * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get
> + * an lsc table interpolated by the LscAlgorithm class for the specified colour
> + * temperature. If the algorithm wants to access the non-interpolated tables it
> + * can retrieve them using LscAlgorithm::getComponents().
> + */
> +
> +/**
> + * \fn LscAlgorithm::init()
> + * \param[in] tuningData The tuning data
> + * \param[in] controls The IPA list of supported controls
> + * \param[in] descriptor The lsc engine descriptor
> + *
> + * Parse \a tuningData according to the settings specified in \a descriptor to
> + * populate the lsc data and registers lsc controls in \a controls.
> + *
> + * \return 0 on success, a negative error code otherwise
> + */
> +
> +/**
> + * \fn LscAlgorithm::configure()
> + * \param[in] state The lsc active state
> + * \param[in] analogCrop The current sensor analog crop rectangle
> + * \param[in] xPos List of horizontal positions of the LSC grid nodes
> + * \param[in] yPos List of vertical positions of the LSC grid nodes

Can xPos and yPos go into init()? I think from the implementation details that they're static rather 
than something determined by the configuration.
> + *
> + * Re-sample the lsc data for an \a analogCrop.
> + *
> + * Lsc data are generated at tuning time using a known sensor configuration.
> + * When a new streaming session is started, it might use a different sensor
> + * configuration for which the lsc tables need to be adjusted to.
> + *
> + * This function re-generates the lsc tables to adapt them to a new sensor
> + * configuration, specifically it re-samples the lsc data for a new \a
> + * analogCrop on a grid specified by \a xPos and \a yPos. Re-sampling of
> + * lsc data is only supported by polynomial-based lsc tables.
> + *
> + * \sa LscImplementation::resampleLscData
> + *
> + * \return 0 on success, a negative error code otherwise
> + */
> +
> +/**
> + * \fn LscAlgorithm::interpolateComponents
> + * \brief Interpolate the lsc tables for a given colour temperature
> + * \param[in] ct The colour temperature
> + *
> + * Lsc data are generated using different colour temperatures during the
> + * tuning phase.
> + *
> + * This function returns the interpolated lsc data for a given \a ct
> + * colour temperature.
> + *
> + * IPA algorithm can use this function to obtain a list of gains per-colour
> + * component to program their lsc engines with every time a significant enough
> + * change in colour temperature is detected.
> + *
> + * Calling this function is only valid after LscAlgorithm::configure() has been
> + * called. An empty components list is returned otherwise.
> + *
> + * \return The lsc gains table interpolated for temperature \a ct
> + */
> +
> +/**
> + * \fn LscAlgorithm::getComponents
> + *
> + * Return the map of lsc data per colour temperature.
> + *
> + * Calling this function is only valid after LscAlgorithm::configure() has been
> + * called. An empty components list is returned otherwise.
> + *
> + * \return The map of lsc gains tables per colour-temperature
> + */
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc.h b/src/ipa/libipa/lsc.h
> new file mode 100644
> index 000000000000..8bfa3a7c4ecf
> --- /dev/null
> +++ b/src/ipa/libipa/lsc.h
> @@ -0,0 +1,145 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026 Ideas on Board Oy
> + *
> + * libIPA Lsc algorithms
> + */
> +
> +#pragma once
> +
> +#include <memory>
> +
> +#include <libcamera/control_ids.h>
> +
> +#include "interpolator.h"
> +#include "lsc_grid.h"
> +#include "lsc_polynomial.h"
> +
> +namespace libcamera {
> +
> +LOG_DECLARE_CATEGORY(Lsc)
> +
> +namespace ipa {
> +
> +namespace lsc {
> +
> +struct ActiveState {
> +	bool enabled;
> +};
> +
> +struct FrameContext {
> +	bool enabled;
> +	bool update;
> +};
> +
> +} /* namespace lsc */
> +
> +#ifndef __DOXYGEN__
> +template<typename T>
> +void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,
> +		       std::vector<T> &dest, double lambda)
> +{
> +	ASSERT(a.size() == b.size());
> +	dest.resize(a.size());
> +	for (size_t i = 0; i < a.size(); i++)
> +		dest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;
> +}
> +
> +template<>
> +void Interpolator<lsc::Components<uint16_t>>::
> +	interpolate(const lsc::Components<uint16_t> &a,
> +		    const lsc::Components<uint16_t> &b,
> +		    lsc::Components<uint16_t> &dest,
> +		    double lambda);
> +#endif /* __DOXYGEN__ */
> +
> +class LscAlgorithmBase
> +{
> +public:
> +	void queueRequest(lsc::ActiveState &state, lsc::FrameContext &context,
> +			  const ControlList &controls);
> +	void process(lsc::FrameContext &context, ControlList &metadata);
> +};
> +
> +template<typename T, typename U>
> +class LscAlgorithm : public LscAlgorithmBase
> +{
> +public:
> +	int init(const ValueNode &tuningData, ControlInfoMap::Map &controls,
> +		 LscDescriptor descriptor)
> +	{
> +		polynomial_ = false;
> +
> +		std::string type = tuningData["type"].get<std::string>("table");
> +		if (type == "table") {
> +			impl_ = std::make_unique<LscGrid<T, U>>();
> +			LOG(Lsc, Debug) << "Using grid-based Lsc";
> +		} else if (type == "polynomial") {
> +			impl_ = std::make_unique<LscPolynomial<T, U>>();
> +			polynomial_ = true;
> +			LOG(Lsc, Debug) << "Using polynomial Lsc";
> +		} else {
> +			LOG(Lsc, Error) << "Unsupported Lsc algorithm '"
> +					<< type << "'";
> +			return -EINVAL;
> +		}
> +
> +		const ValueNode &yamlSets = tuningData["sets"];
> +		if (!yamlSets.isList()) {
> +			LOG(Lsc, Error) << "'sets' parameter not found in tuning file";
> +			return -EINVAL;
> +		}
> +
> +		int ret = impl_->parseLscData(yamlSets, descriptor);
> +		if (ret)
> +			return ret;
> +
> +		controls[&controls::LensShadingCorrectionEnable] =
> +			ControlInfo(false, true, true);
> +
> +		return 0;
> +	}
> +
> +	int configure(lsc::ActiveState &state, const Rectangle &analogCrop,
> +		      const std::vector<double> &xPos,
> +		      const std::vector<double> &yPos)
> +	{
> +		LOG(Lsc, Debug) << "Sample Lsc data for " << analogCrop;
> +		lsc::ComponentsMap<T> lscData =
> +			impl_->resampleLscData(analogCrop, xPos, yPos);
> +
> +		/*
> +		 * Retain a copy of the components table.
> +		 *
> +		 * We could avoid a copy here if getComponents() could
> +		 * return sets_.data() but I wasn't able to work around the
> +		 * compiler refusing it.
> +		 */
> +		lscData_ = lscData;
> +
> +		sets_.setData(std::move(lscData));
> +		state.enabled = true;
> +
> +		return 0;
> +	}
> +
> +	const lsc::Components<T> interpolateComponents(unsigned int ct)
> +	{
> +		return sets_.getInterpolated(ct);
> +	}
> +
> +	const lsc::ComponentsMap<T> getComponents()
> +	{
> +		return lscData_;
> +	}
> +
> +private:
> +	std::unique_ptr<LscImplementation<T, U>> impl_;
> +	Interpolator<lsc::Components<T>> sets_;
> +	lsc::ComponentsMap<T> lscData_;
> +	bool polynomial_;
> +};
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_base.cpp b/src/ipa/libipa/lsc_base.cpp
> new file mode 100644
> index 000000000000..fba8d2f00d54
> --- /dev/null
> +++ b/src/ipa/libipa/lsc_base.cpp
> @@ -0,0 +1,152 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026 Ideas on Board Oy
> + *
> + * Base classes and types for LSC algorithms implementations
> + */
> +
> +#include "lsc_base.h"
> +
> +/**
> + * \file lsc_base.h
> + * \brief Base types and definitions for LscImplementation class hierarchy
> + *
> + * Split the common classes and types definitions to a dedicated file to avoid
> + * circular inclusions.
> + *
> + * Both LscGrid and LscPolynomial inherit from LscImplementation and they are
> + * instantiated from lsc.h as the LscAlgorithm::init() function is there inlined.
> + *
> + * For this reason lsc.h needs to include lsc_grid.h and lsc_polynomial.h which
> + * need the LscImplementation class definition themselves.
> + *
> + * Split the LscImplementation interface definition to this file so that it can
> + * be included by both lsc_grid.h and lsc_polynomial.h, which are then included
> + * by lsc.h.
> + */
> +
> +namespace libcamera {
> +
> +namespace ipa {
> +
> +namespace lsc {
> +
> +/**
> + * \class Components
> + * \brief Associate a colour components with a list of gains
> + * \tparam T The type used to store the gain values when loaded from tuning file
> + *
> + * Lsc tables are defined as a list of gain values associated to a colour
> + * component.
> + *
> + * As different ISP support different colour components (usually 'r', 'gr',
> + * 'gb', 'b' or just 'r', 'g', 'b') this class associates a string
> + * identifier for the colour component to a list of gains of type \a T.
> + *
> + * Each key name shall match an entry in the tuning file and the type \a T
> + * shall match the size of the registers where gains are stored.
> + *
> + * The list of keys is provided to the LscAlgorithm class using \a
> + * LscDescriptor::keys.
> + */
> +
> +/**
> + * \class ComponentsMap
> + * \brief Associate a colour temperature to a lsc table
> + * \tparam T The type used to store the gain values when loaded from tuning file
> + *
> + * An lsc table is generated during the tuning phase for a specific light
> + * temperature, and a tuning file usually contains lsc tables generated for
> + * several different colour temperatures.
> + *
> + * This class associates an lsc table to the colour temperature used when the
> + * table has been generated.
> + */
> +
> +} /* namespace lsc */
> +
> +/**
> + * \struct LscDescriptor
> + * \brief Describe the ISP lsc engine
> + *
> + * \var LscDescriptor::keys
> + * \brief The list of colour components to which a list of gains is associated
> + * with in the tuning file. Used for parsing the tuning file
> + *
> + * \var LscDescriptor::numHCells
> + * \brief Number of horizontal cells of the ISP lsc grid. Used for validating
> + * the list of gains parsed from tuning file
> + *
> + * \var LscDescriptor::numVCells
> + * \brief Number of vertical cells of the ISP lsc grid. Used for validating
> + * the list of gains parsed from tuning file
> + *
> + * \var LscDescriptor::sensorSize
> + * \brief The physical sensor size. This is the largest frame size used to
> + * generate the lsc table. Only used by the polynomial lsc algorithm
> + */
> +
> +/**
> + * \class LscImplementation
> + * \brief Pure virtual base class for lsc algorithm implementations
> + * \tparam T The type used to store the gain values when loaded from tuning file
> + * \tparam U The fixedpoint format used to convert gain values generated by
> + * polynomial expansion to the register format
> + *
> + * Defines the interface for the lsc algorithm implementation. Currently
> + * implemented by LscGrid and LscPolynomial.
> + */
> +
> +/**
> + * \fn LscImplementation::~LscImplementation
> + * \brief Virtual class destructor
> + */
> +
> +/**
> + * \fn LscImplementation::parseLscData
> + * \brief Parse \a tuningData using \a descriptor
> + * \param[in] tuningData The tuning data
> + * \param[in] descriptor The lsc engine descriptor
> + *
> + * Parse the tuning file using the \a descriptor to identify the colour
> + * components in the tuning data and validate the size of the loaded gains
> + * tables.
> + *
> + * \return 0 on success, a negative error number otherwise
> + */
> +
> +/**
> + * \fn LscImplementation::resampleLscData
> + * \brief Re-sample the lsc components for \a cropRectangle
> + * \param[in] cropRectangle The sensor analogue crop rectangle
> + * \param[in] xPos List of horizontal positions of the lsc grid nodes
> + * \param[in] yPos List of vertical positions of the lsc grid nodes
> + *
> + * Lsc tables are expressed in two formats:
> + * - A list of gain values (LscGrid)
> + * - A radial polynomial (LscPolynomial)
> + *
> + * Grid-based lsc data are generated using an image at a fixed resolution and
> + * can't at the moment be re-sampled when a different resolution is used for a
> + * streaming session. Re-sampling a grid lsc table will return the same table
> + * as loaded from the tuning file.
> + *
> + * Polynomial are more flexible and can be re-sampled for a given resolution
> + * using a list of horizontal and vertical nodes that define the lsc grid.
> + * Polynomial lsc tables have to be re-sampled every time a new configuration is
> + * applied, as each streaming session might use a different sensor crop
> + * rectangle.
> + *
> + * \a cropRectangle represents the size of the frame on which the polynomial Lsc
> + * has to be re-sampled on.
> + *
> + * \a xPos and \a yPos represent the position of the grid nodes vertexes in
> + * the [0, 1] interval. In example an equally spaced grid of 16 nodes will have
> + * each segment of size 0.0625 and the list of nodes position will be
> + * [0, 0.0625, 0.125, 0.1875, ... , 1]. It is expected that the first position
> + * is 0 and the last position is 1.
> + */
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
> new file mode 100644
> index 000000000000..aecae305aa9c
> --- /dev/null
> +++ b/src/ipa/libipa/lsc_base.h
> @@ -0,0 +1,59 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026 Ideas on Board Oy
> + *
> + * Base classes and types for LSC algorithms implementations
> + */
> +
> +#pragma once
> +
> +#include <map>
> +#include <string>
> +#include <vector>
> +
> +#include <libcamera/geometry.h>
> +
> +#include "libcamera/internal/value_node.h"
> +
> +namespace libcamera {
> +
> +namespace ipa {
> +
> +namespace lsc {
> +
> +template<typename T>
> +class Components : public std::map<std::string, std::vector<T>>
> +{
> +};
> +
> +template<typename T>
> +class ComponentsMap : public std::map<unsigned int, Components<T>>
> +{
> +};
> +
> +} /* namespace lsc */
> +
> +struct LscDescriptor {
> +	std::vector<std::string> keys;
> +	unsigned int numHCells;
> +	unsigned int numVCells;
> +	Size sensorSize;
> +};
> +
> +template<typename T, typename U>

I'm not sure we need U here. LscImplementation doesn't use it, and LscGrid doesn't use it...and 
removing it like so compiles just fine:

diff --git a/src/ipa/libipa/lsc.h b/src/ipa/libipa/lsc.h
index 8bfa3a7c4..def74cfc8 100644
--- a/src/ipa/libipa/lsc.h
+++ b/src/ipa/libipa/lsc.h
@@ -72,7 +72,7 @@ public:

                 std::string type = tuningData["type"].get<std::string>("table");
                 if (type == "table") {
-                       impl_ = std::make_unique<LscGrid<T, U>>();
+                       impl_ = std::make_unique<LscGrid<T>>();
                         LOG(Lsc, Debug) << "Using grid-based Lsc";
                 } else if (type == "polynomial") {
                         impl_ = std::make_unique<LscPolynomial<T, U>>();
@@ -134,7 +134,7 @@ public:
         }

  private:
-       std::unique_ptr<LscImplementation<T, U>> impl_;
+       std::unique_ptr<LscImplementation<T>> impl_;
         Interpolator<lsc::Components<T>> sets_;
         lsc::ComponentsMap<T> lscData_;
         bool polynomial_;
diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
index aecae305a..c0c7d47e9 100644
--- a/src/ipa/libipa/lsc_base.h
+++ b/src/ipa/libipa/lsc_base.h
@@ -40,7 +40,7 @@ struct LscDescriptor {
         Size sensorSize;
  };

-template<typename T, typename U>
+template<typename T>
  class LscImplementation
  {
  public:
diff --git a/src/ipa/libipa/lsc_grid.h b/src/ipa/libipa/lsc_grid.h
index e92cde95d..95213841a 100644
--- a/src/ipa/libipa/lsc_grid.h
+++ b/src/ipa/libipa/lsc_grid.h
@@ -19,8 +19,8 @@ LOG_DECLARE_CATEGORY(LscGrid)

  namespace ipa {

-template<typename T, typename U>
-class LscGrid : public LscImplementation<T, U>
+template<typename T>
+class LscGrid : public LscImplementation<T>
  {
  public:
         ~LscGrid() {}
diff --git a/src/ipa/libipa/lsc_polynomial.h b/src/ipa/libipa/lsc_polynomial.h
index 72cca591f..482c5fce8 100644
--- a/src/ipa/libipa/lsc_polynomial.h
+++ b/src/ipa/libipa/lsc_polynomial.h
@@ -69,7 +69,7 @@ protected:
  };

  template<typename T, typename U>
-class LscPolynomial : public LscPolynomialBase, public LscImplementation<T, U>
+class LscPolynomial : public LscPolynomialBase, public LscImplementation<T>
  {
  public:
         ~LscPolynomial() {}



So I was going to say "Can we just remove it", buuuuuuut it occurs to me that it will be easier to 
support different hardware with a single implementation of the tuning scripts, if those tuning 
scripts can just return a _gain_ value rather than the quantised gain and then have the algorithm 
return the quantised array to the IPA's implementation...so rather than:

   - Lsc:
       sets:
         - ct: 2500
           r: [
               3461, 3403, 3318, ...
           ]
           g: [
               ...,
           ]
           ...

We have

   - Lsc:
       sets:
         - ct: 2500
           r: [
               3.380, 3.323, 3.240, ...
           ]
           g: [
               ...,
           ]
           ...

And then we send to the IPA implementation the output of U(r[n]).quantized()...what do you think?

> +class LscImplementation
> +{
> +public:
> +	virtual ~LscImplementation() = default;
> +
> +	virtual int parseLscData(const ValueNode &tuningData,
> +				 const LscDescriptor &descriptor) = 0;
> +
> +	virtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> +						      const std::vector<double> &xPos,
> +						      const std::vector<double> &yPos) = 0;
> +};
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_grid.cpp b/src/ipa/libipa/lsc_grid.cpp
> new file mode 100644
> index 000000000000..878994acc8c9
> --- /dev/null
> +++ b/src/ipa/libipa/lsc_grid.cpp
> @@ -0,0 +1,33 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026, Ideas On Board
> + *
> + * Grid based lens shading correction
> + */
> +
> +#include "lsc_grid.h"
> +
> +/**
> + * \file lsc_grid.h
> + * \brief LscGrid class
> + */
> +
> +namespace libcamera {
> +
> +LOG_DEFINE_CATEGORY(LscGrid)
> +
> +namespace ipa {
> +
> +/**
> + * \class LscGrid
> + * \brief Grid based lsc algorithm implementation
> + *
> + * Grid based lsc algorithm implementation. The LscGrid class implements lsc
> + * support using tabular lsc data.
> + *
> + * \sa LscImplementation
> + * \sa LscAlgorithm
> + */
> +
> +} /* namespace ipa */
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_grid.h b/src/ipa/libipa/lsc_grid.h
> new file mode 100644
> index 000000000000..e92cde95d18d
> --- /dev/null
> +++ b/src/ipa/libipa/lsc_grid.h
> @@ -0,0 +1,113 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026, Ideas On Board
> + *
> + * Grid based lens shading correction
> + */
> +
> +#pragma once
> +
> +#include <vector>
> +
> +#include <libcamera/base/log.h>
> +
> +#include "lsc_base.h"
> +
> +namespace libcamera {
> +
> +LOG_DECLARE_CATEGORY(LscGrid)
> +
> +namespace ipa {
> +
> +template<typename T, typename U>
> +class LscGrid : public LscImplementation<T, U>
> +{
> +public:
> +	~LscGrid() {}
> +
> +	int parseLscData(const ValueNode &yamlSets,
> +			 const LscDescriptor &descriptor) override
> +	{
> +		const auto &sets = yamlSets.asList();
> +
> +		for (const auto &yamlSet : sets) {
> +			uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> +
> +			int ret = parseLscComponent(yamlSet, ct, descriptor);
> +			if (ret)
> +				return ret;
> +		}
> +
> +		if (lscData_.empty()) {
> +			LOG(LscGrid, Error) << "Failed to load any sets";
> +			return -EINVAL;
> +		}
> +
> +		return 0;
> +	}
> +
> +	lsc::ComponentsMap<T> resampleLscData([[maybe_unused]] const Rectangle &cropRectangle,
> +					      [[maybe_unused]] const std::vector<double> &xPos,
> +					      [[maybe_unused]] const std::vector<double> &yPos) override
> +	{
> +		/* No resampling for grid-based LSC algorithm. */
> +		return lscData_;
> +	}
> +
> +private:
> +	int parseLscComponent(const ValueNode &yamlSet,
> +			      unsigned int ct, const LscDescriptor &descriptor)
> +	{
> +		lsc::Components<T> component;
> +		for (auto &k : descriptor.keys) {
> +			auto [it, inserted] = component.emplace(
> +				std::piecewise_construct,
> +				std::forward_as_tuple(k.c_str()),
> +				std::forward_as_tuple(parseTable(yamlSet,
> +								 k.c_str(),
> +								 descriptor.numHCells,
> +								 descriptor.numVCells)));
> +			if (!inserted || it->second.empty()) {
> +				LOG(LscGrid, Error)
> +					<< "Set " << k << " for color temperature "
> +					<< ct << " is missing";
> +				return -EINVAL;
> +			}
> +		}
> +
> +		auto [it, inserted] = lscData_.emplace(ct, component);
> +		if (!inserted) {
> +			LOG(LscGrid, Error)
> +				<< "Multiple sets found for color temperature "
> +				<< ct;
> +			return -EINVAL;
> +		}
> +
> +		return 0;
> +	}
> +
> +	std::vector<T> parseTable(const ValueNode &tuningData,
> +				  const char *prop, unsigned int numHCells,
> +				  unsigned int numVCells)
> +	{
> +		unsigned int kLscNumSamples = numHCells * numVCells;
> +
> +		std::vector<T> table =
> +			tuningData[prop].get<std::vector<T>>().value_or(utils::defopt);
> +		if (table.size() != kLscNumSamples) {
> +			LOG(LscGrid, Error)
> +				<< "Invalid '" << prop << "' values: expected "
> +				<< kLscNumSamples
> +				<< " elements, got " << table.size();
> +			return {};
> +		}
> +
> +		return table;
> +	}
> +
> +	lsc::ComponentsMap<T> lscData_;
> +};
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
> index f607d86c54c3..8d3f1883d6a5 100644
> --- a/src/ipa/libipa/lsc_polynomial.cpp
> +++ b/src/ipa/libipa/lsc_polynomial.cpp
> @@ -1,12 +1,14 @@
>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>   /*
> - * Copyright (C) 2024, Ideas On Board
> + * Copyright (C) 2026, Ideas On Board
>    *
> - * Polynomial class to represent lens shading correction
> + * Polynomial based lens shading correction
>    */
>   
>   #include "lsc_polynomial.h"
>   
> +#include <assert.h>
> +
>   #include <libcamera/base/log.h>
>   
>   /**
> @@ -21,7 +23,7 @@ LOG_DEFINE_CATEGORY(LscPolynomial)
>   namespace ipa {
>   
>   /**
> - * \class LscPolynomial
> + * \class Polynomial
>    * \brief Class for handling even polynomials used in lens shading correction
>    *
>    * Shading artifacts of camera lenses can be modeled using even radial
> @@ -31,7 +33,7 @@ namespace ipa {
>    */
>   
>   /**
> - * \fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> + * \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
>   		      double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
>   		      double k4 = 0.0)
>    * \brief Construct a polynomial using the given coefficients
> @@ -45,7 +47,13 @@ namespace ipa {
>    */
>   
>   /**
> - * \fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)
> + * \fn Polynomial::Polynomial(const Polynomial &other)
> + * \brief Construct a Polynomial by copy
> + * \param[in] other The Polynomial to copy construct from
> + */
> +
> +/**
> + * \fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)
>    * \brief Sample the polynomial at the given normalized pixel position
>    *
>    * This functions samples the polynomial at the given pixel position divided by
> @@ -55,9 +63,21 @@ namespace ipa {
>    * \param y y position in normalized coordinates
>    * \return The sampled value
>    */
> +double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
> +{
> +	double dx = x - cnx_;
> +	double dy = y - cny_;
> +	double r = sqrt(dx * dx + dy * dy);
> +	double res = 1.0;
> +
> +	for (unsigned int i = 0; i < coefficients_.size(); i++)
> +		res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> +
> +	return res;
> +}
>   
>   /**
> - * \fn LscPolynomial::getM()
> + * \fn Polynomial::getM()
>    * \brief Get the value m as described in the dng specification
>    *
>    * Returns m according to dng spec. m represents the Euclidean distance
> @@ -66,9 +86,18 @@ namespace ipa {
>    *
>    * \return The sampled value
>    */
> +double Polynomial::getM() const
> +{
> +	double cpx = imageSize_.width * cx_;
> +	double cpy = imageSize_.height * cy_;
> +	double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
> +	double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
> +
> +	return sqrt(mx * mx + my * my);
> +}
>   
>   /**
> - * \fn LscPolynomial::setReferenceImageSize(const Size &size)
> + * \fn Polynomial::setReferenceImageSize(const Size &size)
>    * \brief Set the reference image size
>    *
>    * Set the reference image size that is used for subsequent calls to getM() and
> @@ -76,6 +105,92 @@ namespace ipa {
>    *
>    * \param size The size of the reference image
>    */
> +void Polynomial::setReferenceImageSize(const Size &size)
> +{
> +	assert(!size.isNull());
> +	imageSize_ = size;
> +
> +	/* Calculate normalized centers */
> +	double m = getM();
> +	cnx_ = (size.width * cx_) / m;
> +	cny_ = (size.height * cy_) / m;
> +}
> +
> +/**
> + * \class LscPolynomialBase
> + * \brief Base class for LscPolynomial
> + *
> + * Base class for LscPolynomial for non-templated functions.
> + */
> +
> +/**
> + * \brief Parse polynomial lsc data
> + * \param[in] yamlSets The tuning file content
> + * \param[in] descriptor The lsc engine descriptor
> + *
> + * Parse the lsc data in polyomial form from the \a yamlSet tuning data.
> + */
> +int LscPolynomialBase::parseLscData(const ValueNode &yamlSets,
> +				    const LscDescriptor &descriptor)
> +{
> +	const auto &sets = yamlSets.asList();
> +	for (const auto &yamlSet : sets) {
> +		uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> +
> +		Components components;
> +		for (auto &k : descriptor.keys) {
> +			auto polynomial = yamlSet[k.c_str()].get<Polynomial>();
> +			if (!polynomial) {
> +				LOG(LscPolynomial, Error)
> +					<< "Missing polynomial for component "
> +					<< k;
> +				return -EINVAL;
> +			}
> +
> +			auto [it, inserted] =
> +				components.emplace(std::piecewise_construct,
> +						   std::forward_as_tuple(k.c_str()),
> +						   std::forward_as_tuple(*polynomial));
> +
> +			it->second.setReferenceImageSize(descriptor.sensorSize);
> +		}
> +
> +		auto [it, inserted] = lscData_.emplace(ct, components);
> +		if (!inserted) {
> +			LOG(LscPolynomial, Error)
> +				<< "Multiple sets found for "
> +				<< "color temperature " << ct;
> +			return -EINVAL;
> +		}
> +	}
> +
> +	if (lscData_.empty()) {
> +		LOG(LscPolynomial, Error) << "Failed to load any sets";
> +		return -EINVAL;
> +	}
> +
> +	return 0;
> +}
> +
> +/**
> + * \var LscPolynomialBase::lscData_
> + * \brief The polynomial lsc data
> + *
> + * Maps colour temperatures to per-colour radial polynomial definitions.
> + */
> +
> +/**
> + * \class LscPolynomial
> + * \brief Radial Polynomial lsc algorithm implementation
> + *
> + * Polynomial-based lsc algorithm implementation. The LscPolynomial class
> + * implements lsc support using Polynomial to represent the shading artifacts
> + * map.
> + *
> + * \sa LscImplementation
> + * \sa LscAlgorithm
> + *
> + */
>   
> -} // namespace ipa
> -} // namespace libcamera
> +} /* namespace ipa */
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_polynomial.h b/src/ipa/libipa/lsc_polynomial.h
> index d7d9ae42e360..72cca591f260 100644
> --- a/src/ipa/libipa/lsc_polynomial.h
> +++ b/src/ipa/libipa/lsc_polynomial.h
> @@ -1,15 +1,17 @@
>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>   /*
> - * Copyright (C) 2024, Ideas On Board
> + * Copyright (C) 2026, Ideas On Board
>    *
> - * Helper for radial polynomial used in lens shading correction.
> + * Polynomial based lens shading correction
>    */
> +
>   #pragma once
>   
>   #include <algorithm>
>   #include <array>
> -#include <assert.h>
>   #include <cmath>
> +#include <optional>
> +#include <tuple>
>   
>   #include <libcamera/base/log.h>
>   #include <libcamera/base/span.h>
> @@ -18,64 +20,117 @@
>   
>   #include "libcamera/internal/value_node.h"
>   
> +#include "lsc_base.h"
> +
>   namespace libcamera {
>   
>   LOG_DECLARE_CATEGORY(LscPolynomial)
>   
>   namespace ipa {
>   
> -class LscPolynomial
> +class Polynomial
>   {
>   public:
> -	LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> -		      double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> -		      double k4 = 0.0)
> +	Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> +		   double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> +		   double k4 = 0.0)
>   		: cx_(cx), cy_(cy), cnx_(0), cny_(0),
>   		  coefficients_({ k0, k1, k2, k3, k4 })
>   	{
>   	}
>   
> -	double sampleAtNormalizedPixelPos(double x, double y) const
> +	Polynomial(const Polynomial &other) = default;
> +
> +	double sampleAtNormalizedPixelPos(double x, double y) const;
> +	double getM() const;
> +	void setReferenceImageSize(const Size &size);
> +
> +private:
> +	double cx_;
> +	double cy_;
> +	double cnx_;
> +	double cny_;
> +	std::array<double, 5> coefficients_;
> +	Size imageSize_;
> +};
> +
> +class LscPolynomialBase
> +{
> +private:
> +	using Components = std::map<std::string, Polynomial>;
> +	using ComponentsMap = std::map<unsigned int, Components>;
> +
> +public:
> +	int parseLscData(const ValueNode &yamlSets,
> +			 const LscDescriptor &descriptor);
> +
> +protected:
> +	ComponentsMap lscData_;
> +};
> +
> +template<typename T, typename U>
> +class LscPolynomial : public LscPolynomialBase, public LscImplementation<T, U>
> +{
> +public:
> +	~LscPolynomial() {}
> +
> +	int parseLscData(const ValueNode &yamlSets,
> +			 const LscDescriptor &descriptor) override
>   	{
> -		double dx = x - cnx_;
> -		double dy = y - cny_;
> -		double r = sqrt(dx * dx + dy * dy);
> -		double res = 1.0;
> -		for (unsigned int i = 0; i < coefficients_.size(); i++) {
> -			res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> -		}
> -		return res;
> +		return LscPolynomialBase::parseLscData(yamlSets, descriptor);
>   	}
>   
> -	double getM() const
> +	lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> +					      const std::vector<double> &xPos,
> +					      const std::vector<double> &yPos) override
>   	{
> -		double cpx = imageSize_.width * cx_;
> -		double cpy = imageSize_.height * cy_;
> -		double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
> -		double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
> +		lsc::ComponentsMap<T> components;
>   
> -		return sqrt(mx * mx + my * my);
> -	}
> +		for (auto &[t, c] : lscData_) {
> +			lsc::Components<T> comp;
>   
> -	void setReferenceImageSize(const Size &size)
> -	{
> -		assert(!size.isNull());
> -		imageSize_ = size;
> +			for (auto &[k, p] : c) {
> +				comp.emplace(std::piecewise_construct,
> +					     std::forward_as_tuple(k),
> +					     std::forward_as_tuple(samplePolynomial(p, xPos, yPos,
> +										    cropRectangle)));
> +			}
>   
> -		/* Calculate normalized centers */
> -		double m = getM();
> -		cnx_ = (size.width * cx_) / m;
> -		cny_ = (size.height * cy_) / m;
> +			components[t] = comp;
> +		}
> +
> +		return components;
>   	}
>   
>   private:
> -	double cx_;
> -	double cy_;
> -	double cnx_;
> -	double cny_;
> -	std::array<double, 5> coefficients_;
> +	std::vector<T> samplePolynomial(const Polynomial &poly,
> +					Span<const double> xPositions,
> +					Span<const double> yPositions,
> +					const Rectangle &cropRectangle)
>   
> -	Size imageSize_;
> +	{
> +		double m = poly.getM();
> +		double x0 = cropRectangle.x / m;
> +		double y0 = cropRectangle.y / m;
> +		double w = cropRectangle.width / m;
> +		double h = cropRectangle.height / m;
> +		std::vector<T> samples;
> +
> +		samples.reserve(xPositions.size() * yPositions.size());
> +
> +		for (double y : yPositions) {
> +			for (double x : xPositions) {
> +				double xp = x0 + x * w;
> +				double yp = y0 + y * h;
> +				float sample = static_cast<float>
> +						(poly.sampleAtNormalizedPixelPos(xp, yp));
> +
> +				samples.push_back(U(sample).quantized());
> +			}
> +		}
> +
> +		return samples;
> +	}
>   };
>   
>   } /* namespace ipa */
> @@ -83,8 +138,8 @@ private:
>   #ifndef __DOXYGEN__
>   
>   template<>
> -struct ValueNode::Accessor<ipa::LscPolynomial> {
> -	std::optional<ipa::LscPolynomial> get(const ValueNode &obj) const
> +struct ValueNode::Accessor<ipa::Polynomial> {
> +	std::optional<ipa::Polynomial> get(const ValueNode &obj) const
>   	{
>   		std::optional<double> cx = obj["cx"].get<double>();
>   		std::optional<double> cy = obj["cy"].get<double>();
> @@ -98,7 +153,7 @@ struct ValueNode::Accessor<ipa::LscPolynomial> {
>   			LOG(LscPolynomial, Error)
>   				<< "Polynomial is missing a parameter";
>   
> -		return ipa::LscPolynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);
> +		return ipa::Polynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);
>   	}
>   };
>   
> diff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build
> index edf8eabd8b78..da6ea0c5e130 100644
> --- a/src/ipa/libipa/meson.build
> +++ b/src/ipa/libipa/meson.build
> @@ -14,6 +14,9 @@ libipa_headers = files([
>       'fixedpoint.h',
>       'histogram.h',
>       'interpolator.h',
> +    'lsc.h',
> +    'lsc_base.h',
> +    'lsc_grid.h',
>       'lsc_polynomial.h',
>       'lux.h',
>       'module.h',
> @@ -36,6 +39,8 @@ libipa_sources = files([
>       'fixedpoint.cpp',
>       'histogram.cpp',
>       'interpolator.cpp',
> +    'lsc.cpp',
> +    'lsc_grid.cpp',
>       'lsc_polynomial.cpp',
>       'lux.cpp',
>       'module.cpp',
> diff --git a/src/ipa/rkisp1/algorithms/lsc.cpp b/src/ipa/rkisp1/algorithms/lsc.cpp
> index 25f7c512b35c..51076737ffa5 100644
> --- a/src/ipa/rkisp1/algorithms/lsc.cpp
> +++ b/src/ipa/rkisp1/algorithms/lsc.cpp
> @@ -2,7 +2,7 @@
>   /*
>    * Copyright (C) 2021-2022, Ideas On Board
>    *
> - * RkISP1 Lens Shading Correction control
> + * RkISP1 Lens Shading Correction algorithm
>    */
>   
>   #include "lsc.h"
> @@ -14,56 +14,17 @@
>   #include <libcamera/base/log.h>
>   #include <libcamera/base/utils.h>
>   
> -#include "libcamera/internal/value_node.h"
> -
> -#include "libipa/lsc_polynomial.h"
> -#include "linux/rkisp1-config.h"
> -
>   /**
>    * \file lsc.h
>    */
>   
>   namespace libcamera {
>   
> -namespace ipa {
> -
> -template<typename T>
> -void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,
> -		       std::vector<T> &dest, double lambda)
> -{
> -	ASSERT(a.size() == b.size());
> -	dest.resize(a.size());
> -	for (size_t i = 0; i < a.size(); i++)
> -		dest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;
> -}
> -
> -template<>
> -void Interpolator<rkisp1::algorithms::LensShadingCorrection::Components>::
> -	interpolate(const rkisp1::algorithms::LensShadingCorrection::Components &a,
> -		    const rkisp1::algorithms::LensShadingCorrection::Components &b,
> -		    rkisp1::algorithms::LensShadingCorrection::Components &dest,
> -		    double lambda)
> -{
> -	interpolateVector(a.r, b.r, dest.r, lambda);
> -	interpolateVector(a.gr, b.gr, dest.gr, lambda);
> -	interpolateVector(a.gb, b.gb, dest.gb, lambda);
> -	interpolateVector(a.b, b.b, dest.b, lambda);
> -}
> -
> -} /* namespace ipa */
> -
>   namespace ipa::rkisp1::algorithms {
>   
>   /**
>    * \class LensShadingCorrection
>    * \brief RkISP1 Lens Shading Correction control
> - *
> - * Due to the optical characteristics of the lens, the light intensity received
> - * by the sensor is not uniform.
> - *
> - * The Lens Shading Correction algorithm applies multipliers to all pixels
> - * to compensate for the lens shading effect. The coefficients are
> - * specified in a downscaled table in the YAML tuning file.
>    */
>   
>   LOG_DEFINE_CATEGORY(RkISP1Lsc)
> @@ -72,265 +33,20 @@ namespace {
>   
>   constexpr int kColourTemperatureQuantization = 10;
>   
> -class LscPolynomialShadingDescriptor : public LensShadingCorrection::ShadingDescriptor
> -{
> -public:
> -	LscPolynomialShadingDescriptor(const LscPolynomial &pr, const LscPolynomial &pgr,
> -				       const LscPolynomial &pgb, const LscPolynomial &pb)
> -		: pr_(pr), pgr_(pgr), pgb_(pgb), pb_(pb)
> -	{
> -	}
> -
> -	LensShadingCorrection::Components sampleForCrop(const Rectangle &cropRectangle,
> -							Span<const double> xSizes,
> -							Span<const double> ySizes) override;
> -
> -private:
> -	std::vector<uint16_t> samplePolynomial(const LscPolynomial &poly,
> -					       Span<const double> xPositions,
> -					       Span<const double> yPositions,
> -					       const Rectangle &cropRectangle);
> -
> -	std::vector<double> sizesListToPositions(Span<const double> sizes);
> -
> -	LscPolynomial pr_;
> -	LscPolynomial pgr_;
> -	LscPolynomial pgb_;
> -	LscPolynomial pb_;
> -};
> -
> -LensShadingCorrection::Components
> -LscPolynomialShadingDescriptor::sampleForCrop(const Rectangle &cropRectangle,
> -					      Span<const double> xSizes,
> -					      Span<const double> ySizes)
> -{
> -	std::vector<double> xPos = sizesListToPositions(xSizes);
> -	std::vector<double> yPos = sizesListToPositions(ySizes);
> -
> -	return {
> -		.r = samplePolynomial(pr_, xPos, yPos, cropRectangle),
> -		.gr = samplePolynomial(pgr_, xPos, yPos, cropRectangle),
> -		.gb = samplePolynomial(pgb_, xPos, yPos, cropRectangle),
> -		.b = samplePolynomial(pb_, xPos, yPos, cropRectangle)
> -	};
> -}
> -
> -std::vector<uint16_t>
> -LscPolynomialShadingDescriptor::samplePolynomial(const LscPolynomial &poly,
> -						 Span<const double> xPositions,
> -						 Span<const double> yPositions,
> -						 const Rectangle &cropRectangle)
> -{
> -	double m = poly.getM();
> -	double x0 = cropRectangle.x / m;
> -	double y0 = cropRectangle.y / m;
> -	double w = cropRectangle.width / m;
> -	double h = cropRectangle.height / m;
> -	std::vector<uint16_t> samples;
> -
> -	samples.reserve(xPositions.size() * yPositions.size());
> -
> -	for (double y : yPositions) {
> -		for (double x : xPositions) {
> -			double xp = x0 + x * w;
> -			double yp = y0 + y * h;
> -			/*
> -			 * The hardware uses 2.10 fixed point format and limits
> -			 * the legal values to [1..3.999]. Scale and clamp the
> -			 * sampled value accordingly.
> -			 */
> -			int v = static_cast<int>(
> -				poly.sampleAtNormalizedPixelPos(xp, yp) *
> -				1024);
> -			v = std::clamp(v, 1024, 4095);
> -			samples.push_back(v);
> -		}
> -	}
> -	return samples;
> -}
> -
> -/*
> - * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left
> - * quadrant of the grid. This is then mirrored in hardware to the other
> - * quadrants. See parseSizes() for further details. For easier handling, this
> - * function converts the cell sizes of half the grid to a list of position of
> - * the whole grid (on one axis). Example:
> - *
> - * input:   | 0.2 | 0.3 |
> - * output: 0.0   0.2   0.5   0.8   1.0
> - */
> -std::vector<double>
> -LscPolynomialShadingDescriptor::sizesListToPositions(Span<const double> sizes)
> -{
> -	const int half = sizes.size();
> -	std::vector<double> positions(half * 2 + 1);
> -	double x = 0.0;
> -
> -	positions[half] = 0.5;
> -	for (int i = 1; i <= half; i++) {
> -		x += sizes[half - i];
> -		positions[half - i] = 0.5 - x;
> -		positions[half + i] = 0.5 + x;
> -	}
> -
> -	return positions;
> -}
> -
> -class LscPolynomialLoader
> -{
> -public:
> -	LscPolynomialLoader(const Size &sensorSize)
> -		: sensorSize_(sensorSize)
> -	{
> -	}
> -
> -	int parseLscData(const ValueNode &yamlSets,
> -			 LensShadingCorrection::ShadingDescriptorMap &lscData);
> -
> -private:
> -	Size sensorSize_;
> -};
> -
> -int LscPolynomialLoader::parseLscData(const ValueNode &yamlSets,
> -				      LensShadingCorrection::ShadingDescriptorMap &lscData)
> +unsigned int quantize(unsigned int value, unsigned int step)

I ended up using this in the IPU3 implementation too; perhaps it can move into the LscAlgorithm 
somewhere...though that can happen on-top of course.

That's my last comment. It's really nice; implementing the IPU3 lens shading algorithm was a real 
breeze using this, so I think it works very very well at the goal of reducing the amount of 
duplicated code between the IPAs. I think we should settle the question above of whether to have 
"raw" gain or quantized gains (which are IPA module specific) in the tuning files and then this is 
good to go.


>   {
> -	const auto &sets = yamlSets.asList();
> -	for (const auto &yamlSet : sets) {
> -		std::optional<LscPolynomial> pr, pgr, pgb, pb;
> -		uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> -
> -		if (lscData.count(ct)) {
> -			LOG(RkISP1Lsc, Error)
> -				<< "Multiple sets found for "
> -				<< "color temperature " << ct;
> -			return -EINVAL;
> -		}
> -
> -		pr = yamlSet["r"].get<LscPolynomial>();
> -		pgr = yamlSet["gr"].get<LscPolynomial>();
> -		pgb = yamlSet["gb"].get<LscPolynomial>();
> -		pb = yamlSet["b"].get<LscPolynomial>();
> -
> -		if (!(pr || pgr || pgb || pb)) {
> -			LOG(RkISP1Lsc, Error)
> -				<< "Failed to parse polynomial for "
> -				<< "colour temperature " << ct;
> -			return -EINVAL;
> -		}
> -
> -		pr->setReferenceImageSize(sensorSize_);
> -		pgr->setReferenceImageSize(sensorSize_);
> -		pgb->setReferenceImageSize(sensorSize_);
> -		pb->setReferenceImageSize(sensorSize_);
> -
> -		lscData.emplace(
> -			ct, std::make_unique<LscPolynomialShadingDescriptor>(
> -				    *pr, *pgr, *pgb, *pb));
> -	}
> -
> -	if (lscData.empty()) {
> -		LOG(RkISP1Lsc, Error) << "Failed to load any sets";
> -		return -EINVAL;
> -	}
> -
> -	return 0;
> +	return std::lround(value / static_cast<double>(step)) * step;
>   }
>   
> -class LscTableShadingDescriptor : public LensShadingCorrection::ShadingDescriptor
> -{
> -public:
> -	LscTableShadingDescriptor(LensShadingCorrection::Components components)
> -		: lscData_(std::move(components))
> -	{
> -	}
> -
> -	LensShadingCorrection::Components
> -	sampleForCrop([[maybe_unused]] const Rectangle &cropRectangle,
> -		      [[maybe_unused]] Span<const double> xSizes,
> -		      [[maybe_unused]] Span<const double> ySizes) override
> -	{
> -		LOG(RkISP1Lsc, Warning)
> -			<< "Tabular LSC data doesn't support resampling";
> -		return lscData_;
> -	}
> -
> -private:
> -	LensShadingCorrection::Components lscData_;
> -};
> -
> -class LscTableLoader
> -{
> -public:
> -	int parseLscData(const ValueNode &yamlSets,
> -			 LensShadingCorrection::ShadingDescriptorMap &lscData);
> -
> -private:
> -	std::vector<uint16_t> parseTable(const ValueNode &tuningData,
> -					 const char *prop);
> -};
> -
> -int LscTableLoader::parseLscData(const ValueNode &yamlSets,
> -				 LensShadingCorrection::ShadingDescriptorMap &lscData)
> -{
> -	const auto &sets = yamlSets.asList();
> -
> -	for (const auto &yamlSet : sets) {
> -		uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> -
> -		if (lscData.count(ct)) {
> -			LOG(RkISP1Lsc, Error)
> -				<< "Multiple sets found for color temperature "
> -				<< ct;
> -			return -EINVAL;
> -		}
> -
> -		LensShadingCorrection::Components set;
> -		set.r = parseTable(yamlSet, "r");
> -		set.gr = parseTable(yamlSet, "gr");
> -		set.gb = parseTable(yamlSet, "gb");
> -		set.b = parseTable(yamlSet, "b");
> -
> -		if (set.r.empty() || set.gr.empty() ||
> -		    set.gb.empty() || set.b.empty()) {
> -			LOG(RkISP1Lsc, Error)
> -				<< "Set for color temperature " << ct
> -				<< " is missing tables";
> -			return -EINVAL;
> -		}
> -
> -		lscData.emplace(
> -			ct, std::make_unique<LscTableShadingDescriptor>(std::move(set)));
> -	}
> -
> -	if (lscData.empty()) {
> -		LOG(RkISP1Lsc, Error) << "Failed to load any sets";
> -		return -EINVAL;
> -	}
> -
> -	return 0;
> -}
> +} /* namespace */
>   
> -std::vector<uint16_t> LscTableLoader::parseTable(const ValueNode &tuningData,
> -						 const char *prop)
> +LensShadingCorrection::LensShadingCorrection()
> +	: lastAppliedCt_(0), lastAppliedQuantizedCt_(0)
>   {
> -	static constexpr unsigned int kLscNumSamples =
> -		RKISP1_CIF_ISP_LSC_SAMPLES_MAX * RKISP1_CIF_ISP_LSC_SAMPLES_MAX;
> -
> -	std::vector<uint16_t> table =
> -		tuningData[prop].get<std::vector<uint16_t>>().value_or(utils::defopt);
> -	if (table.size() != kLscNumSamples) {
> -		LOG(RkISP1Lsc, Error)
> -			<< "Invalid '" << prop << "' values: expected "
> -			<< kLscNumSamples
> -			<< " elements, got " << table.size();
> -		return {};
> -	}
> -
> -	return table;
>   }
>   
> -std::vector<double> parseSizes(const ValueNode &tuningData,
> -			       const char *prop)
> +std::vector<double> LensShadingCorrection::parseSizes(const ValueNode &tuningData,
> +						      const char *prop)
>   {
>   	std::vector<double> sizes =
>   		tuningData[prop].get<std::vector<double>>().value_or(utils::defopt);
> @@ -359,22 +75,36 @@ std::vector<double> parseSizes(const ValueNode &tuningData,
>   	return sizes;
>   }
>   
> -unsigned int quantize(unsigned int value, unsigned int step)
> +/*
> + * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left
> + * quadrant of the grid. This is then mirrored in hardware to the other
> + * quadrants. See parseSizes() for further details. For easier handling, this
> + * function converts the cell sizes of half the grid to a list of position of
> + * the whole grid (on one axis). Example:
> + *
> + * input:   | 0.2 | 0.3 |
> + * output: 0.0   0.2   0.5   0.8   1.0
> + */
> +std::vector<double> LensShadingCorrection::sizesToPositions(Span<const double> sizes)
>   {
> -	return std::lround(value / static_cast<double>(step)) * step;
> -}
> +	const int half = sizes.size();
> +	std::vector<double> positions(half * 2 + 1);
> +	double x = 0.0;
>   
> -} /* namespace */
> +	positions[half] = 0.5;
> +	for (int i = 1; i <= half; i++) {
> +		x += sizes[half - i];
> +		positions[half - i] = 0.5 - x;
> +		positions[half + i] = 0.5 + x;
> +	}
>   
> -LensShadingCorrection::LensShadingCorrection()
> -	: lastAppliedCt_(0), lastAppliedQuantizedCt_(0)
> -{
> +	return positions;
>   }
>   
>   /**
>    * \copydoc libcamera::ipa::Algorithm::init
>    */
> -int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
> +int LensShadingCorrection::init(IPAContext &context,
>   				const ValueNode &tuningData)
>   {
>   	xSize_ = parseSizes(tuningData, "x-size");
> @@ -383,56 +113,27 @@ int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
>   	if (xSize_.empty() || ySize_.empty())
>   		return -EINVAL;
>   
> -	/* Get all defined sets to apply. */
> -	const ValueNode &yamlSets = tuningData["sets"];
> -	if (!yamlSets.isList()) {
> -		LOG(RkISP1Lsc, Error)
> -			<< "'sets' parameter not found in tuning file";
> -		return -EINVAL;
> -	}
> -
> -	ShadingDescriptorMap lscData;
> -	int ret = 0;
> -
> -	std::string type = tuningData["type"].get<std::string>("table");
> -	if (type == "table") {
> -		LOG(RkISP1Lsc, Debug) << "Loading tabular LSC data.";
> -		auto loader = LscTableLoader();
> -		ret = loader.parseLscData(yamlSets, lscData);
> -	} else if (type == "polynomial") {
> -		LOG(RkISP1Lsc, Debug) << "Loading polynomial LSC data.";
> -		/*
> -		 * \todo: Most likely the reference frame should be native_size.
> -		 * Let's wait how the internal discussions progress.
> -		 */
> -		auto loader = LscPolynomialLoader(context.sensorInfo.activeAreaSize);
> -		ret = loader.parseLscData(yamlSets, lscData);
> -	} else {
> -		LOG(RkISP1Lsc, Error) << "Unsupported LSC data type '"
> -				      << type << "'";
> -		ret = -EINVAL;
> -	}
> -
> -	if (ret)
> -		return ret;
> +	xPos_ = sizesToPositions(xSize_);
> +	yPos_ = sizesToPositions(ySize_);
>   
> -	context.ctrlMap[&controls::LensShadingCorrectionEnable] =
> -		ControlInfo(false, true, true);
> -
> -	shadingDescriptors_ = std::move(lscData);
> -
> -	return 0;
> +	return lscAlgo_.init(tuningData, context.ctrlMap, {
> +				.keys = { "r", "gr", "gb", "b" },
> +				.numHCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,
> +				.numVCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,
> +				.sensorSize = context.sensorInfo.activeAreaSize
> +			     });
>   }
>   
>   /**
>    * \copydoc libcamera::ipa::Algorithm::configure
>    */
>   int LensShadingCorrection::configure(IPAContext &context,
> -				     [[maybe_unused]] const IPACameraSensorInfo &configInfo)
> +				     const IPACameraSensorInfo &configInfo)
>   {
>   	const Size &size = context.configuration.sensor.size;
>   	Size totalSize{};
>   
> +	/* Calculate gradients. */
>   	for (unsigned int i = 0; i < RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE; ++i) {
>   		xSizes_[i] = xSize_[i] * size.width;
>   		ySizes_[i] = ySize_[i] * size.height;
> @@ -455,16 +156,8 @@ int LensShadingCorrection::configure(IPAContext &context,
>   		yGrad_[i] = std::round(32768 / ySizes_[i]);
>   	}
>   
> -	LOG(RkISP1Lsc, Debug) << "Sample LSC data for " << configInfo.analogCrop;
> -	std::map<unsigned int, LensShadingCorrection::Components> shadingData;
> -	for (const auto &[t, descriptor] : shadingDescriptors_)
> -		shadingData[t] = descriptor->sampleForCrop(configInfo.analogCrop,
> -							   xSize_, ySize_);
> -
> -	sets_.setData(std::move(shadingData));
> -
> -	context.activeState.lsc.enabled = true;
> -	return 0;
> +	return lscAlgo_.configure(context.activeState.lsc, configInfo.analogCrop,
> +				  xPos_, yPos_);
>   }
>   
>   void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)
> @@ -476,12 +169,16 @@ void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)
>   }
>   
>   void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
> -				      const Components &set)
> +				      const ipa::lsc::Components<uint16_t> &set)
>   {
> -	std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
> -	std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
> -	std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
> -	std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
> +	const auto &r = set.at("r");
> +	std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
> +	const auto &gr = set.at("gr");
> +	std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
> +	const auto &gb = set.at("gb");
> +	std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
> +	const auto &b = set.at("b");
> +	std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
>   }
>   
>   /**
> @@ -492,19 +189,8 @@ void LensShadingCorrection::queueRequest(IPAContext &context,
>   					 IPAFrameContext &frameContext,
>   					 const ControlList &controls)
>   {
> -	auto &lsc = context.activeState.lsc;
> -
> -	const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);
> -	if (lscEnable && *lscEnable != lsc.enabled) {
> -		lsc.enabled = *lscEnable;
> -
> -		LOG(RkISP1Lsc, Debug)
> -			<< (lsc.enabled ? "Enabling" : "Disabling") << " Lsc";
> -
> -		frameContext.lsc.update = true;
> -	}
> -
> -	frameContext.lsc.enabled = lsc.enabled;
> +	lscAlgo_.queueRequest(context.activeState.lsc, frameContext.lsc,
> +			      controls);
>   }
>   
>   /**
> @@ -542,7 +228,7 @@ void LensShadingCorrection::prepare([[maybe_unused]] IPAContext &context,
>   
>   	setParameters(*config);
>   
> -	const Components &set = sets_.getInterpolated(quantizedCt);
> +	const auto &set = lscAlgo_.interpolateComponents(quantizedCt);
>   	copyTable(*config, set);
>   
>   	lastAppliedCt_ = ct;
> @@ -562,7 +248,7 @@ void LensShadingCorrection::process([[maybe_unused]] IPAContext &context,
>   				    [[maybe_unused]] const rkisp1_stat_buffer *stats,
>   				    ControlList &metadata)
>   {
> -	metadata.set(controls::LensShadingCorrectionEnable, frameContext.lsc.enabled);
> +	lscAlgo_.process(frameContext.lsc, metadata);
>   }
>   
>   REGISTER_IPA_ALGORITHM(LensShadingCorrection, "LensShadingCorrection")
> diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
> index 0a256e225327..7c656d95acfb 100644
> --- a/src/ipa/rkisp1/algorithms/lsc.h
> +++ b/src/ipa/rkisp1/algorithms/lsc.h
> @@ -2,17 +2,23 @@
>   /*
>    * Copyright (C) 2021-2022, Ideas On Board
>    *
> - * RkISP1 Lens Shading Correction control
> + * RkISP1 Lens Shading Correction algorithm
>    */
>   
>   #pragma once
>   
> -#include <map>
> -#include <memory>
> +#include <vector>
>   
> -#include "libipa/interpolator.h"
> +#include <linux/rkisp1-config.h>
> +
> +#include "libcamera/internal/value_node.h"
> +
> +#include "libipa/fixedpoint.h"
> +#include "libipa/lsc.h"
>   
>   #include "algorithm.h"
> +#include "ipa_context.h"
> +#include "params.h"
>   
>   namespace libcamera {
>   
> @@ -37,39 +43,28 @@ public:
>   		     const rkisp1_stat_buffer *stats,
>   		     ControlList &metadata) override;
>   
> -	struct Components {
> -		std::vector<uint16_t> r;
> -		std::vector<uint16_t> gr;
> -		std::vector<uint16_t> gb;
> -		std::vector<uint16_t> b;
> -	};
> -
> -	class ShadingDescriptor
> -	{
> -	public:
> -		virtual ~ShadingDescriptor() = default;
> -		virtual Components sampleForCrop(const Rectangle &cropRectangle,
> -						 Span<const double> xSizes,
> -						 Span<const double> ySizes) = 0;
> -	};
> -
> -	using ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;
> -
>   private:
> +	std::vector<double> parseSizes(const ValueNode &tuningData,
> +				       const char *prop);
> +	std::vector<double> sizesToPositions(Span<const double> sizes);
> +
>   	void setParameters(rkisp1_cif_isp_lsc_config &config);
> -	void copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);
> +	void copyTable(rkisp1_cif_isp_lsc_config &config,
> +		       const ipa::lsc::Components<uint16_t> &set0);
>   
> -	ShadingDescriptorMap shadingDescriptors_;
> -	ipa::Interpolator<Components> sets_;
>   	std::vector<double> xSize_;
>   	std::vector<double> ySize_;
>   	uint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
>   	uint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
>   	uint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
>   	uint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> +	std::vector<double> xPos_;
> +	std::vector<double> yPos_;
>   
>   	unsigned int lastAppliedCt_;
>   	unsigned int lastAppliedQuantizedCt_;
> +
> +	LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;
>   };
>   
>   } /* namespace ipa::rkisp1::algorithms */
> diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
> index cd97e10bcf2b..005f4102b4f6 100644
> --- a/src/ipa/rkisp1/ipa_context.h
> +++ b/src/ipa/rkisp1/ipa_context.h
> @@ -30,6 +30,7 @@
>   #include "libipa/ccm.h"
>   #include "libipa/fc_queue.h"
>   #include "libipa/fixedpoint.h"
> +#include "libipa/lsc.h"
>   
>   namespace libcamera {
>   
> @@ -138,9 +139,7 @@ struct IPAActiveState {
>   		double strength;
>   	} wdr;
>   
> -	struct {
> -		bool enabled;
> -	} lsc;
> +	ipa::lsc::ActiveState lsc;
>   };
>   
>   struct IPAFrameContext : public FrameContext {
> @@ -213,10 +212,7 @@ struct IPAFrameContext : public FrameContext {
>   		double gain;
>   	} wdr;
>   
> -	struct {
> -		bool enabled;
> -		bool update;
> -	} lsc;
> +	ipa::lsc::FrameContext lsc;
>   };
>   
>   struct IPAContext {
>
Jacopo Mondi June 23, 2026, 12:39 p.m. UTC | #3
Hi Dan

On Tue, Jun 23, 2026 at 10:30:40AM +0100, Dan Scally wrote:
> Morning Jacopo
>
> On 15/06/2026 15:05, Jacopo Mondi wrote:
> > Introduce an Lsc algorithm implementation in libIPA.
> >
> > The LscAlgorithm class supports two algorithms: grid-based lsc and
> > polynomial-based lsc.
> >
> > Both implementations have been generalized from the existing ones in the
> > RkISP1 IPA.
> >
> > Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> > ---
> >   src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++
> >   src/ipa/libipa/lsc.h              | 145 +++++++++++++
> >   src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++
> >   src/ipa/libipa/lsc_base.h         |  59 ++++++
> >   src/ipa/libipa/lsc_grid.cpp       |  33 +++
> >   src/ipa/libipa/lsc_grid.h         | 113 ++++++++++
> >   src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-
> >   src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----
> >   src/ipa/libipa/meson.build        |   5 +
> >   src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------
> >   src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--
> >   src/ipa/rkisp1/ipa_context.h      |  10 +-
> >   12 files changed, 1126 insertions(+), 450 deletions(-)
> >
> > diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
> > new file mode 100644
> > index 000000000000..ae713a776778
> > --- /dev/null
> > +++ b/src/ipa/libipa/lsc.cpp
> > @@ -0,0 +1,322 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2026 Ideas on Board Oy
> > + *
> > + * libIPA Lsc algorithms
> > + */
> > +
> > +#include "lsc.h"
> > +
> > +#include <libcamera/base/log.h>
> > +
> > +/**
> > + * \file lsc.h
> > + * \brief libipa lsc algorithm
> > + */
> > +
> > +namespace libcamera {
> > +
> > +LOG_DEFINE_CATEGORY(Lsc)
> > +
> > +namespace ipa {
> > +
> > +namespace lsc {
> > +
> > +/**
> > + * \struct ActiveState
> > + * \brief The lsc active state
> > + *
> > + * \var ActiveState::enabled
> > + * \brief Boolean flag for the LscAlgorithm enable status
> > + */
> > +
> > +/**
> > + * \struct FrameContext
> > + * \brief The lsc frame context
> > + *
> > + * \var FrameContext::enabled
> > + * \brief Boolean flag for the LscAlgorithm enable status
> > + *
> > + * \var FrameContext::update
> > + * \brief Boolean flag for the LscAlgorithm updated status
> > + */
> > +
> > +} /* namespace lsc */
> > +
> > +#ifndef __DOXYGEN__
> > +template<>
> > +void Interpolator<lsc::Components<uint16_t>>::
> > +	interpolate(const lsc::Components<uint16_t> &a,
> > +		    const lsc::Components<uint16_t> &b,
> > +		    lsc::Components<uint16_t> &dest,
> > +		    double lambda)
> > +{
> > +	for (auto const &[k, v] : a)
> > +		interpolateVector(v, b.at(k), dest[k], lambda);
> > +}
> > +#endif
> > +
> > +/**
> > + * \class LscAlgorithmBase
> > + * \brief Base class for LscAlgorithm
> > + *
> > + * Base class for LscAlgorithm for non-templated functions implementation
> > + */
> > +
> > +/**
> > + * \brief Queue a request to the lsc algorithm
> > + * \param[in] state The lsc active state
> > + * \param[in] context The lsc frame context
> > + * \param[in] controls The list of controls associated with a Request
> > + *
> > + * Queue a new list of \a controls to the lsc algorithm.
> > + * The only supported control is controls::LensShadingCorrectionEnable.
> > + */
> > +void LscAlgorithmBase::queueRequest(lsc::ActiveState &state,
> > +				    lsc::FrameContext &context,
> > +				    const ControlList &controls)
> > +{
> > +	const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);
> > +	if (lscEnable && *lscEnable != state.enabled) {
> > +		state.enabled = *lscEnable;
> > +
> > +		LOG(Lsc, Debug)
> > +			<< (state.enabled ? "Enabling" : "Disabling") << " Lsc";
> > +
> > +		context.update = true;
> > +	}
> > +
> > +	context.enabled = state.enabled;
> > +}
> > +
> > +/**
> > + * \brief Populate the list of lsc metadata
> > + * \param[in] context The lsc frame context
> > + * \param[in] metadata The list of metadata
> > + *
> > + * Populates the list of \a metadata with controls handled by the LscAlgorithm
> > + * class. The only supported metadata is controls::LensShadingCorrectionEnable.
> > + */
> > +void LscAlgorithmBase::process(lsc::FrameContext &context, ControlList &metadata)
> > +{
> > +	metadata.set(controls::LensShadingCorrectionEnable, context.enabled);
> > +}
> > +
> > +/**
> > + * \class LscAlgorithm
> > + * \brief libIPA lsc algorithm implementation
> > + * \tparam T The type used to store the gain values when loaded from tuning file
> > + * \tparam U The fixedpoint lsc engine register format
> > + *
> > + * Due to the optical characteristics of the lens, the light intensity received
> > + * by the sensor is not uniform. The Lens Shading Correction algorithm applies
> > + * multipliers to all pixels to compensate for the lens shading effect.
> > + *
> > + * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.
> > + * It provides support for parsing the tuning file content and generate tables
>
> s/generate/generates?
>

ack

> > + * of per-colour temperature gains that IPA algorithms can use to program their
> > + * lsc engine.
> > + *
> > + * The init() function parses the tuning file and loads the gain tables either
> > + * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain
> > + * tables are organized per-colour temperature with per-colour components gain
> > + * vectors or polynomial coefficients. The colour components names are
> > + * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc
> > + * engine support 4 colour components (r, gr, gb, b), some only support 3 colour
> > + * components (r, g, b). The name (and number) of the expected colour components
> > + * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys
> > + * field.
> > + *
> > + * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour
> > + * components. The gain table has been here omitted, but the expected number
> > + * of entries has be equal to
> > + * LscDescriptor::numHCells * LscDescriptor::numVCells.
> > + *
> > + * \code{.yaml}
> > + * - Lsc:
> > + *    sets:
> > + *      - ct: 2500
> > + *        r: [
> > + *        	.. gains table omitted..
> > + *        ]
> > + *        g: [
> > + *        	.. gains table omitted..
> > + *        ]
> > + *        b: [
> > + *        	.. gains table omitted..
> > + *        ]
> > + *      - ct: 6500
> > + *        r: [
> > + *        	.. gains table omitted..
> > + *        ]
> > + *        g: [
> > + *        	.. gains table omitted..
> > + *        ]
> > + *        b: [
> > + *        	.. gains table omitted..
> > + *        ]
> > + * \endcode
> > + *
> > + * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'
> > + * colour components:
> > + *
> > + * \code{.yaml}
> > + * - Lsc:
> > + *    type: "polynomial"
> > + *    sets:
> > + *      - ct: 2500
> > + *        r:
> > + *          cx: 0.5006571711950275
> > + *          cy: 0.510093737499277
> > + *          k0: 1.5393282208428813
> > + *          k1: -1.1434559757908016
> > + *          k2: 4.332602305814554
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + *        gr:
> > + *          cx: 0.5009320529087338
> > + *          cy: 0.511208038949085
> > + *          k0: 1.5634738574805407
> > + *          k1: -1.5623484259968348
> > + *          k2: 4.846686073656501
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + *        gb:
> > + *          cx: 0.5012013290343839
> > + *          cy: 0.5128251541578288
> > + *          k0: 1.526147944919103
> > + *          k1: -1.4316976083689723
> > + *          k2: 4.792604063222728
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + *        b:
> > + *          cx: 0.49864139511067784
> > + *          cy: 0.5162095081739346
> > + *          k0: 1.0405245474038738
> > + *          k1: 0.05618339879447103
> > + *          k2: 1.8792813594001752
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + *      - ct: 6000
> > + *        r:
> > + *          cx: 0.5006202239353942
> > + *          cy: 0.5099531318307661
> > + *          k0: 1.4702946023945032
> > + *          k1: -0.8893767547927631
> > + *          k2: 3.920547732201387
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + *        gr:
> > + *          cx: 0.500907874178317
> > + *          cy: 0.511084916024106
> > + *          k0: 1.5336172760559457
> > + *          k1: -1.39964026514435
> > + *          k2: 4.565487728954618
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + *        gb:
> > + *          cx: 0.5011898608900477
> > + *          cy: 0.5126797906745105
> > + *          k0: 1.5013145790354843
> > + *          k1: -1.2747407173754124
> > + *          k2: 4.514682876897286
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + *        b:
> > + *          cx: 0.4987561413116136
> > + *          cy: 0.5159619420778772
> > + *          k0: 1.0102986422191802
> > + *          k1: 0.13263449763985727
> > + *          k2: 1.686556107316064
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + * \endcode
> > + *
> > + * The lsc tables or the polynomial definition are generated at tuning time
> > + * using an image of known resolution which needs to be specified in
> > + * LscDescriptor::sensorSize.
> > + *
> > + * At LscAlgorithm::configure() time the lsc tables are re-sampled on the
> > + * sensor's crop rectangle in use to adapt them to the configuration in use for
> > + * a streaming session. Polynomial lsc tables support re-sampling and can be
> > + * applied to any sensor configuration. Grid-based lsc tables cannot be
> > + * re-sampled and the configuration as parsed from the tuning file is used for
> > + * all sensor configurations providing best-effort results.
> > + *
> > + * When the IPA algorithms wants to get access to the (resampled) tables to
> > + * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get
> > + * an lsc table interpolated by the LscAlgorithm class for the specified colour
> > + * temperature. If the algorithm wants to access the non-interpolated tables it
> > + * can retrieve them using LscAlgorithm::getComponents().
> > + */
> > +
> > +/**
> > + * \fn LscAlgorithm::init()
> > + * \param[in] tuningData The tuning data
> > + * \param[in] controls The IPA list of supported controls
> > + * \param[in] descriptor The lsc engine descriptor
> > + *
> > + * Parse \a tuningData according to the settings specified in \a descriptor to
> > + * populate the lsc data and registers lsc controls in \a controls.
> > + *
> > + * \return 0 on success, a negative error code otherwise
> > + */
> > +
> > +/**
> > + * \fn LscAlgorithm::configure()
> > + * \param[in] state The lsc active state
> > + * \param[in] analogCrop The current sensor analog crop rectangle
> > + * \param[in] xPos List of horizontal positions of the LSC grid nodes
> > + * \param[in] yPos List of vertical positions of the LSC grid nodes
>
> Can xPos and yPos go into init()? I think from the implementation details
> that they're static rather than something determined by the configuration.

Can't xPos and yPos depend on the grid positioning on a new sensor
crop rectangle ? Isn't it worth to allow to specify them at configure
time ?

I know rkisp1 currently doesn't do that and feed the algorithms with
the same grid, but as this is performed at configure() time I presume
there was a use case envisioned for that ?


> > + *
> > + * Re-sample the lsc data for an \a analogCrop.
> > + *
> > + * Lsc data are generated at tuning time using a known sensor configuration.
> > + * When a new streaming session is started, it might use a different sensor
> > + * configuration for which the lsc tables need to be adjusted to.
> > + *
> > + * This function re-generates the lsc tables to adapt them to a new sensor
> > + * configuration, specifically it re-samples the lsc data for a new \a
> > + * analogCrop on a grid specified by \a xPos and \a yPos. Re-sampling of
> > + * lsc data is only supported by polynomial-based lsc tables.
> > + *
> > + * \sa LscImplementation::resampleLscData
> > + *
> > + * \return 0 on success, a negative error code otherwise
> > + */
> > +
> > +/**
> > + * \fn LscAlgorithm::interpolateComponents
> > + * \brief Interpolate the lsc tables for a given colour temperature
> > + * \param[in] ct The colour temperature
> > + *
> > + * Lsc data are generated using different colour temperatures during the
> > + * tuning phase.
> > + *
> > + * This function returns the interpolated lsc data for a given \a ct
> > + * colour temperature.
> > + *
> > + * IPA algorithm can use this function to obtain a list of gains per-colour
> > + * component to program their lsc engines with every time a significant enough
> > + * change in colour temperature is detected.
> > + *
> > + * Calling this function is only valid after LscAlgorithm::configure() has been
> > + * called. An empty components list is returned otherwise.
> > + *
> > + * \return The lsc gains table interpolated for temperature \a ct
> > + */
> > +
> > +/**
> > + * \fn LscAlgorithm::getComponents
> > + *
> > + * Return the map of lsc data per colour temperature.
> > + *
> > + * Calling this function is only valid after LscAlgorithm::configure() has been
> > + * called. An empty components list is returned otherwise.
> > + *
> > + * \return The map of lsc gains tables per colour-temperature
> > + */
> > +
> > +} /* namespace ipa */
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/ipa/libipa/lsc.h b/src/ipa/libipa/lsc.h
> > new file mode 100644
> > index 000000000000..8bfa3a7c4ecf
> > --- /dev/null
> > +++ b/src/ipa/libipa/lsc.h
> > @@ -0,0 +1,145 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2026 Ideas on Board Oy
> > + *
> > + * libIPA Lsc algorithms
> > + */
> > +
> > +#pragma once
> > +
> > +#include <memory>
> > +
> > +#include <libcamera/control_ids.h>
> > +
> > +#include "interpolator.h"
> > +#include "lsc_grid.h"
> > +#include "lsc_polynomial.h"
> > +
> > +namespace libcamera {
> > +
> > +LOG_DECLARE_CATEGORY(Lsc)
> > +
> > +namespace ipa {
> > +
> > +namespace lsc {
> > +
> > +struct ActiveState {
> > +	bool enabled;
> > +};
> > +
> > +struct FrameContext {
> > +	bool enabled;
> > +	bool update;
> > +};
> > +
> > +} /* namespace lsc */
> > +
> > +#ifndef __DOXYGEN__
> > +template<typename T>
> > +void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,
> > +		       std::vector<T> &dest, double lambda)
> > +{
> > +	ASSERT(a.size() == b.size());
> > +	dest.resize(a.size());
> > +	for (size_t i = 0; i < a.size(); i++)
> > +		dest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;
> > +}
> > +
> > +template<>
> > +void Interpolator<lsc::Components<uint16_t>>::
> > +	interpolate(const lsc::Components<uint16_t> &a,
> > +		    const lsc::Components<uint16_t> &b,
> > +		    lsc::Components<uint16_t> &dest,
> > +		    double lambda);
> > +#endif /* __DOXYGEN__ */
> > +
> > +class LscAlgorithmBase
> > +{
> > +public:
> > +	void queueRequest(lsc::ActiveState &state, lsc::FrameContext &context,
> > +			  const ControlList &controls);
> > +	void process(lsc::FrameContext &context, ControlList &metadata);
> > +};
> > +
> > +template<typename T, typename U>
> > +class LscAlgorithm : public LscAlgorithmBase
> > +{
> > +public:
> > +	int init(const ValueNode &tuningData, ControlInfoMap::Map &controls,
> > +		 LscDescriptor descriptor)
> > +	{
> > +		polynomial_ = false;
> > +
> > +		std::string type = tuningData["type"].get<std::string>("table");
> > +		if (type == "table") {
> > +			impl_ = std::make_unique<LscGrid<T, U>>();
> > +			LOG(Lsc, Debug) << "Using grid-based Lsc";
> > +		} else if (type == "polynomial") {
> > +			impl_ = std::make_unique<LscPolynomial<T, U>>();
> > +			polynomial_ = true;
> > +			LOG(Lsc, Debug) << "Using polynomial Lsc";
> > +		} else {
> > +			LOG(Lsc, Error) << "Unsupported Lsc algorithm '"
> > +					<< type << "'";
> > +			return -EINVAL;
> > +		}
> > +
> > +		const ValueNode &yamlSets = tuningData["sets"];
> > +		if (!yamlSets.isList()) {
> > +			LOG(Lsc, Error) << "'sets' parameter not found in tuning file";
> > +			return -EINVAL;
> > +		}
> > +
> > +		int ret = impl_->parseLscData(yamlSets, descriptor);
> > +		if (ret)
> > +			return ret;
> > +
> > +		controls[&controls::LensShadingCorrectionEnable] =
> > +			ControlInfo(false, true, true);
> > +
> > +		return 0;
> > +	}
> > +
> > +	int configure(lsc::ActiveState &state, const Rectangle &analogCrop,
> > +		      const std::vector<double> &xPos,
> > +		      const std::vector<double> &yPos)
> > +	{
> > +		LOG(Lsc, Debug) << "Sample Lsc data for " << analogCrop;
> > +		lsc::ComponentsMap<T> lscData =
> > +			impl_->resampleLscData(analogCrop, xPos, yPos);
> > +
> > +		/*
> > +		 * Retain a copy of the components table.
> > +		 *
> > +		 * We could avoid a copy here if getComponents() could
> > +		 * return sets_.data() but I wasn't able to work around the
> > +		 * compiler refusing it.
> > +		 */
> > +		lscData_ = lscData;
> > +
> > +		sets_.setData(std::move(lscData));
> > +		state.enabled = true;
> > +
> > +		return 0;
> > +	}
> > +
> > +	const lsc::Components<T> interpolateComponents(unsigned int ct)
> > +	{
> > +		return sets_.getInterpolated(ct);
> > +	}
> > +
> > +	const lsc::ComponentsMap<T> getComponents()
> > +	{
> > +		return lscData_;
> > +	}
> > +
> > +private:
> > +	std::unique_ptr<LscImplementation<T, U>> impl_;
> > +	Interpolator<lsc::Components<T>> sets_;
> > +	lsc::ComponentsMap<T> lscData_;
> > +	bool polynomial_;
> > +};
> > +
> > +} /* namespace ipa */
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/ipa/libipa/lsc_base.cpp b/src/ipa/libipa/lsc_base.cpp
> > new file mode 100644
> > index 000000000000..fba8d2f00d54
> > --- /dev/null
> > +++ b/src/ipa/libipa/lsc_base.cpp
> > @@ -0,0 +1,152 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2026 Ideas on Board Oy
> > + *
> > + * Base classes and types for LSC algorithms implementations
> > + */
> > +
> > +#include "lsc_base.h"
> > +
> > +/**
> > + * \file lsc_base.h
> > + * \brief Base types and definitions for LscImplementation class hierarchy
> > + *
> > + * Split the common classes and types definitions to a dedicated file to avoid
> > + * circular inclusions.
> > + *
> > + * Both LscGrid and LscPolynomial inherit from LscImplementation and they are
> > + * instantiated from lsc.h as the LscAlgorithm::init() function is there inlined.
> > + *
> > + * For this reason lsc.h needs to include lsc_grid.h and lsc_polynomial.h which
> > + * need the LscImplementation class definition themselves.
> > + *
> > + * Split the LscImplementation interface definition to this file so that it can
> > + * be included by both lsc_grid.h and lsc_polynomial.h, which are then included
> > + * by lsc.h.
> > + */
> > +
> > +namespace libcamera {
> > +
> > +namespace ipa {
> > +
> > +namespace lsc {
> > +
> > +/**
> > + * \class Components
> > + * \brief Associate a colour components with a list of gains
> > + * \tparam T The type used to store the gain values when loaded from tuning file
> > + *
> > + * Lsc tables are defined as a list of gain values associated to a colour
> > + * component.
> > + *
> > + * As different ISP support different colour components (usually 'r', 'gr',
> > + * 'gb', 'b' or just 'r', 'g', 'b') this class associates a string
> > + * identifier for the colour component to a list of gains of type \a T.
> > + *
> > + * Each key name shall match an entry in the tuning file and the type \a T
> > + * shall match the size of the registers where gains are stored.
> > + *
> > + * The list of keys is provided to the LscAlgorithm class using \a
> > + * LscDescriptor::keys.
> > + */
> > +
> > +/**
> > + * \class ComponentsMap
> > + * \brief Associate a colour temperature to a lsc table
> > + * \tparam T The type used to store the gain values when loaded from tuning file
> > + *
> > + * An lsc table is generated during the tuning phase for a specific light
> > + * temperature, and a tuning file usually contains lsc tables generated for
> > + * several different colour temperatures.
> > + *
> > + * This class associates an lsc table to the colour temperature used when the
> > + * table has been generated.
> > + */
> > +
> > +} /* namespace lsc */
> > +
> > +/**
> > + * \struct LscDescriptor
> > + * \brief Describe the ISP lsc engine
> > + *
> > + * \var LscDescriptor::keys
> > + * \brief The list of colour components to which a list of gains is associated
> > + * with in the tuning file. Used for parsing the tuning file
> > + *
> > + * \var LscDescriptor::numHCells
> > + * \brief Number of horizontal cells of the ISP lsc grid. Used for validating
> > + * the list of gains parsed from tuning file
> > + *
> > + * \var LscDescriptor::numVCells
> > + * \brief Number of vertical cells of the ISP lsc grid. Used for validating
> > + * the list of gains parsed from tuning file
> > + *
> > + * \var LscDescriptor::sensorSize
> > + * \brief The physical sensor size. This is the largest frame size used to
> > + * generate the lsc table. Only used by the polynomial lsc algorithm
> > + */
> > +
> > +/**
> > + * \class LscImplementation
> > + * \brief Pure virtual base class for lsc algorithm implementations
> > + * \tparam T The type used to store the gain values when loaded from tuning file
> > + * \tparam U The fixedpoint format used to convert gain values generated by
> > + * polynomial expansion to the register format
> > + *
> > + * Defines the interface for the lsc algorithm implementation. Currently
> > + * implemented by LscGrid and LscPolynomial.
> > + */
> > +
> > +/**
> > + * \fn LscImplementation::~LscImplementation
> > + * \brief Virtual class destructor
> > + */
> > +
> > +/**
> > + * \fn LscImplementation::parseLscData
> > + * \brief Parse \a tuningData using \a descriptor
> > + * \param[in] tuningData The tuning data
> > + * \param[in] descriptor The lsc engine descriptor
> > + *
> > + * Parse the tuning file using the \a descriptor to identify the colour
> > + * components in the tuning data and validate the size of the loaded gains
> > + * tables.
> > + *
> > + * \return 0 on success, a negative error number otherwise
> > + */
> > +
> > +/**
> > + * \fn LscImplementation::resampleLscData
> > + * \brief Re-sample the lsc components for \a cropRectangle
> > + * \param[in] cropRectangle The sensor analogue crop rectangle
> > + * \param[in] xPos List of horizontal positions of the lsc grid nodes
> > + * \param[in] yPos List of vertical positions of the lsc grid nodes
> > + *
> > + * Lsc tables are expressed in two formats:
> > + * - A list of gain values (LscGrid)
> > + * - A radial polynomial (LscPolynomial)
> > + *
> > + * Grid-based lsc data are generated using an image at a fixed resolution and
> > + * can't at the moment be re-sampled when a different resolution is used for a
> > + * streaming session. Re-sampling a grid lsc table will return the same table
> > + * as loaded from the tuning file.
> > + *
> > + * Polynomial are more flexible and can be re-sampled for a given resolution
> > + * using a list of horizontal and vertical nodes that define the lsc grid.
> > + * Polynomial lsc tables have to be re-sampled every time a new configuration is
> > + * applied, as each streaming session might use a different sensor crop
> > + * rectangle.
> > + *
> > + * \a cropRectangle represents the size of the frame on which the polynomial Lsc
> > + * has to be re-sampled on.
> > + *
> > + * \a xPos and \a yPos represent the position of the grid nodes vertexes in
> > + * the [0, 1] interval. In example an equally spaced grid of 16 nodes will have
> > + * each segment of size 0.0625 and the list of nodes position will be
> > + * [0, 0.0625, 0.125, 0.1875, ... , 1]. It is expected that the first position
> > + * is 0 and the last position is 1.
> > + */
> > +
> > +} /* namespace ipa */
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
> > new file mode 100644
> > index 000000000000..aecae305aa9c
> > --- /dev/null
> > +++ b/src/ipa/libipa/lsc_base.h
> > @@ -0,0 +1,59 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2026 Ideas on Board Oy
> > + *
> > + * Base classes and types for LSC algorithms implementations
> > + */
> > +
> > +#pragma once
> > +
> > +#include <map>
> > +#include <string>
> > +#include <vector>
> > +
> > +#include <libcamera/geometry.h>
> > +
> > +#include "libcamera/internal/value_node.h"
> > +
> > +namespace libcamera {
> > +
> > +namespace ipa {
> > +
> > +namespace lsc {
> > +
> > +template<typename T>
> > +class Components : public std::map<std::string, std::vector<T>>
> > +{
> > +};
> > +
> > +template<typename T>
> > +class ComponentsMap : public std::map<unsigned int, Components<T>>
> > +{
> > +};
> > +
> > +} /* namespace lsc */
> > +
> > +struct LscDescriptor {
> > +	std::vector<std::string> keys;
> > +	unsigned int numHCells;
> > +	unsigned int numVCells;
> > +	Size sensorSize;
> > +};
> > +
> > +template<typename T, typename U>
>
> I'm not sure we need U here. LscImplementation doesn't use it, and LscGrid
> doesn't use it...and removing it like so compiles just fine:
>
> diff --git a/src/ipa/libipa/lsc.h b/src/ipa/libipa/lsc.h
> index 8bfa3a7c4..def74cfc8 100644
> --- a/src/ipa/libipa/lsc.h
> +++ b/src/ipa/libipa/lsc.h
> @@ -72,7 +72,7 @@ public:
>
>                 std::string type = tuningData["type"].get<std::string>("table");
>                 if (type == "table") {
> -                       impl_ = std::make_unique<LscGrid<T, U>>();
> +                       impl_ = std::make_unique<LscGrid<T>>();
>                         LOG(Lsc, Debug) << "Using grid-based Lsc";
>                 } else if (type == "polynomial") {
>                         impl_ = std::make_unique<LscPolynomial<T, U>>();
> @@ -134,7 +134,7 @@ public:
>         }
>
>  private:
> -       std::unique_ptr<LscImplementation<T, U>> impl_;
> +       std::unique_ptr<LscImplementation<T>> impl_;
>         Interpolator<lsc::Components<T>> sets_;
>         lsc::ComponentsMap<T> lscData_;
>         bool polynomial_;
> diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
> index aecae305a..c0c7d47e9 100644
> --- a/src/ipa/libipa/lsc_base.h
> +++ b/src/ipa/libipa/lsc_base.h
> @@ -40,7 +40,7 @@ struct LscDescriptor {
>         Size sensorSize;
>  };
>
> -template<typename T, typename U>
> +template<typename T>
>  class LscImplementation
>  {
>  public:
> diff --git a/src/ipa/libipa/lsc_grid.h b/src/ipa/libipa/lsc_grid.h
> index e92cde95d..95213841a 100644
> --- a/src/ipa/libipa/lsc_grid.h
> +++ b/src/ipa/libipa/lsc_grid.h
> @@ -19,8 +19,8 @@ LOG_DECLARE_CATEGORY(LscGrid)
>
>  namespace ipa {
>
> -template<typename T, typename U>
> -class LscGrid : public LscImplementation<T, U>
> +template<typename T>
> +class LscGrid : public LscImplementation<T>
>  {
>  public:
>         ~LscGrid() {}
> diff --git a/src/ipa/libipa/lsc_polynomial.h b/src/ipa/libipa/lsc_polynomial.h
> index 72cca591f..482c5fce8 100644
> --- a/src/ipa/libipa/lsc_polynomial.h
> +++ b/src/ipa/libipa/lsc_polynomial.h
> @@ -69,7 +69,7 @@ protected:
>  };
>
>  template<typename T, typename U>
> -class LscPolynomial : public LscPolynomialBase, public LscImplementation<T, U>
> +class LscPolynomial : public LscPolynomialBase, public LscImplementation<T>
>  {
>  public:
>         ~LscPolynomial() {}
>
>
>
> So I was going to say "Can we just remove it", buuuuuuut it occurs to me
> that it will be easier to support different hardware with a single
> implementation of the tuning scripts, if those tuning scripts can just
> return a _gain_ value rather than the quantised gain and then have the
> algorithm return the quantised array to the IPA's implementation...so rather
> than:
>
>   - Lsc:
>       sets:
>         - ct: 2500
>           r: [
>               3461, 3403, 3318, ...
>           ]
>           g: [
>               ...,
>           ]
>           ...
>
> We have
>
>   - Lsc:
>       sets:
>         - ct: 2500
>           r: [
>               3.380, 3.323, 3.240, ...
>           ]
>           g: [
>               ...,
>           ]
>           ...
>
> And then we send to the IPA implementation the output of U(r[n]).quantized()...what do you think?
>

I think that's the direction where we want to go

However it is not clear to me how you would remove U even with this
version, as LscPolynomial uses it in samplePolynomial()

		for (double y : yPositions) {
			for (double x : xPositions) {
				double xp = x0 + x * w;
				double yp = y0 + y * h;
				float sample = static_cast<float>
						(poly.sampleAtNormalizedPixelPos(xp, yp));

				samples.push_back(U(sample).quantized());
			}
		}

What have I missed ?


> > +class LscImplementation
> > +{
> > +public:
> > +	virtual ~LscImplementation() = default;
> > +
> > +	virtual int parseLscData(const ValueNode &tuningData,
> > +				 const LscDescriptor &descriptor) = 0;
> > +
> > +	virtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> > +						      const std::vector<double> &xPos,
> > +						      const std::vector<double> &yPos) = 0;
> > +};
> > +
> > +} /* namespace ipa */
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/ipa/libipa/lsc_grid.cpp b/src/ipa/libipa/lsc_grid.cpp
> > new file mode 100644
> > index 000000000000..878994acc8c9
> > --- /dev/null
> > +++ b/src/ipa/libipa/lsc_grid.cpp
> > @@ -0,0 +1,33 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2026, Ideas On Board
> > + *
> > + * Grid based lens shading correction
> > + */
> > +
> > +#include "lsc_grid.h"
> > +
> > +/**
> > + * \file lsc_grid.h
> > + * \brief LscGrid class
> > + */
> > +
> > +namespace libcamera {
> > +
> > +LOG_DEFINE_CATEGORY(LscGrid)
> > +
> > +namespace ipa {
> > +
> > +/**
> > + * \class LscGrid
> > + * \brief Grid based lsc algorithm implementation
> > + *
> > + * Grid based lsc algorithm implementation. The LscGrid class implements lsc
> > + * support using tabular lsc data.
> > + *
> > + * \sa LscImplementation
> > + * \sa LscAlgorithm
> > + */
> > +
> > +} /* namespace ipa */
> > +} /* namespace libcamera */
> > diff --git a/src/ipa/libipa/lsc_grid.h b/src/ipa/libipa/lsc_grid.h
> > new file mode 100644
> > index 000000000000..e92cde95d18d
> > --- /dev/null
> > +++ b/src/ipa/libipa/lsc_grid.h
> > @@ -0,0 +1,113 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2026, Ideas On Board
> > + *
> > + * Grid based lens shading correction
> > + */
> > +
> > +#pragma once
> > +
> > +#include <vector>
> > +
> > +#include <libcamera/base/log.h>
> > +
> > +#include "lsc_base.h"
> > +
> > +namespace libcamera {
> > +
> > +LOG_DECLARE_CATEGORY(LscGrid)
> > +
> > +namespace ipa {
> > +
> > +template<typename T, typename U>
> > +class LscGrid : public LscImplementation<T, U>
> > +{
> > +public:
> > +	~LscGrid() {}
> > +
> > +	int parseLscData(const ValueNode &yamlSets,
> > +			 const LscDescriptor &descriptor) override
> > +	{
> > +		const auto &sets = yamlSets.asList();
> > +
> > +		for (const auto &yamlSet : sets) {
> > +			uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> > +
> > +			int ret = parseLscComponent(yamlSet, ct, descriptor);
> > +			if (ret)
> > +				return ret;
> > +		}
> > +
> > +		if (lscData_.empty()) {
> > +			LOG(LscGrid, Error) << "Failed to load any sets";
> > +			return -EINVAL;
> > +		}
> > +
> > +		return 0;
> > +	}
> > +
> > +	lsc::ComponentsMap<T> resampleLscData([[maybe_unused]] const Rectangle &cropRectangle,
> > +					      [[maybe_unused]] const std::vector<double> &xPos,
> > +					      [[maybe_unused]] const std::vector<double> &yPos) override
> > +	{
> > +		/* No resampling for grid-based LSC algorithm. */
> > +		return lscData_;
> > +	}
> > +
> > +private:
> > +	int parseLscComponent(const ValueNode &yamlSet,
> > +			      unsigned int ct, const LscDescriptor &descriptor)
> > +	{
> > +		lsc::Components<T> component;
> > +		for (auto &k : descriptor.keys) {
> > +			auto [it, inserted] = component.emplace(
> > +				std::piecewise_construct,
> > +				std::forward_as_tuple(k.c_str()),
> > +				std::forward_as_tuple(parseTable(yamlSet,
> > +								 k.c_str(),
> > +								 descriptor.numHCells,
> > +								 descriptor.numVCells)));
> > +			if (!inserted || it->second.empty()) {
> > +				LOG(LscGrid, Error)
> > +					<< "Set " << k << " for color temperature "
> > +					<< ct << " is missing";
> > +				return -EINVAL;
> > +			}
> > +		}
> > +
> > +		auto [it, inserted] = lscData_.emplace(ct, component);
> > +		if (!inserted) {
> > +			LOG(LscGrid, Error)
> > +				<< "Multiple sets found for color temperature "
> > +				<< ct;
> > +			return -EINVAL;
> > +		}
> > +
> > +		return 0;
> > +	}
> > +
> > +	std::vector<T> parseTable(const ValueNode &tuningData,
> > +				  const char *prop, unsigned int numHCells,
> > +				  unsigned int numVCells)
> > +	{
> > +		unsigned int kLscNumSamples = numHCells * numVCells;
> > +
> > +		std::vector<T> table =
> > +			tuningData[prop].get<std::vector<T>>().value_or(utils::defopt);
> > +		if (table.size() != kLscNumSamples) {
> > +			LOG(LscGrid, Error)
> > +				<< "Invalid '" << prop << "' values: expected "
> > +				<< kLscNumSamples
> > +				<< " elements, got " << table.size();
> > +			return {};
> > +		}
> > +
> > +		return table;
> > +	}
> > +
> > +	lsc::ComponentsMap<T> lscData_;
> > +};
> > +
> > +} /* namespace ipa */
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
> > index f607d86c54c3..8d3f1883d6a5 100644
> > --- a/src/ipa/libipa/lsc_polynomial.cpp
> > +++ b/src/ipa/libipa/lsc_polynomial.cpp
> > @@ -1,12 +1,14 @@
> >   /* SPDX-License-Identifier: LGPL-2.1-or-later */
> >   /*
> > - * Copyright (C) 2024, Ideas On Board
> > + * Copyright (C) 2026, Ideas On Board
> >    *
> > - * Polynomial class to represent lens shading correction
> > + * Polynomial based lens shading correction
> >    */
> >   #include "lsc_polynomial.h"
> > +#include <assert.h>
> > +
> >   #include <libcamera/base/log.h>
> >   /**
> > @@ -21,7 +23,7 @@ LOG_DEFINE_CATEGORY(LscPolynomial)
> >   namespace ipa {
> >   /**
> > - * \class LscPolynomial
> > + * \class Polynomial
> >    * \brief Class for handling even polynomials used in lens shading correction
> >    *
> >    * Shading artifacts of camera lenses can be modeled using even radial
> > @@ -31,7 +33,7 @@ namespace ipa {
> >    */
> >   /**
> > - * \fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > + * \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> >   		      double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> >   		      double k4 = 0.0)
> >    * \brief Construct a polynomial using the given coefficients
> > @@ -45,7 +47,13 @@ namespace ipa {
> >    */
> >   /**
> > - * \fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)
> > + * \fn Polynomial::Polynomial(const Polynomial &other)
> > + * \brief Construct a Polynomial by copy
> > + * \param[in] other The Polynomial to copy construct from
> > + */
> > +
> > +/**
> > + * \fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)
> >    * \brief Sample the polynomial at the given normalized pixel position
> >    *
> >    * This functions samples the polynomial at the given pixel position divided by
> > @@ -55,9 +63,21 @@ namespace ipa {
> >    * \param y y position in normalized coordinates
> >    * \return The sampled value
> >    */
> > +double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
> > +{
> > +	double dx = x - cnx_;
> > +	double dy = y - cny_;
> > +	double r = sqrt(dx * dx + dy * dy);
> > +	double res = 1.0;
> > +
> > +	for (unsigned int i = 0; i < coefficients_.size(); i++)
> > +		res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> > +
> > +	return res;
> > +}
> >   /**
> > - * \fn LscPolynomial::getM()
> > + * \fn Polynomial::getM()
> >    * \brief Get the value m as described in the dng specification
> >    *
> >    * Returns m according to dng spec. m represents the Euclidean distance
> > @@ -66,9 +86,18 @@ namespace ipa {
> >    *
> >    * \return The sampled value
> >    */
> > +double Polynomial::getM() const
> > +{
> > +	double cpx = imageSize_.width * cx_;
> > +	double cpy = imageSize_.height * cy_;
> > +	double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
> > +	double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
> > +
> > +	return sqrt(mx * mx + my * my);
> > +}
> >   /**
> > - * \fn LscPolynomial::setReferenceImageSize(const Size &size)
> > + * \fn Polynomial::setReferenceImageSize(const Size &size)
> >    * \brief Set the reference image size
> >    *
> >    * Set the reference image size that is used for subsequent calls to getM() and
> > @@ -76,6 +105,92 @@ namespace ipa {
> >    *
> >    * \param size The size of the reference image
> >    */
> > +void Polynomial::setReferenceImageSize(const Size &size)
> > +{
> > +	assert(!size.isNull());
> > +	imageSize_ = size;
> > +
> > +	/* Calculate normalized centers */
> > +	double m = getM();
> > +	cnx_ = (size.width * cx_) / m;
> > +	cny_ = (size.height * cy_) / m;
> > +}
> > +
> > +/**
> > + * \class LscPolynomialBase
> > + * \brief Base class for LscPolynomial
> > + *
> > + * Base class for LscPolynomial for non-templated functions.
> > + */
> > +
> > +/**
> > + * \brief Parse polynomial lsc data
> > + * \param[in] yamlSets The tuning file content
> > + * \param[in] descriptor The lsc engine descriptor
> > + *
> > + * Parse the lsc data in polyomial form from the \a yamlSet tuning data.
> > + */
> > +int LscPolynomialBase::parseLscData(const ValueNode &yamlSets,
> > +				    const LscDescriptor &descriptor)
> > +{
> > +	const auto &sets = yamlSets.asList();
> > +	for (const auto &yamlSet : sets) {
> > +		uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> > +
> > +		Components components;
> > +		for (auto &k : descriptor.keys) {
> > +			auto polynomial = yamlSet[k.c_str()].get<Polynomial>();
> > +			if (!polynomial) {
> > +				LOG(LscPolynomial, Error)
> > +					<< "Missing polynomial for component "
> > +					<< k;
> > +				return -EINVAL;
> > +			}
> > +
> > +			auto [it, inserted] =
> > +				components.emplace(std::piecewise_construct,
> > +						   std::forward_as_tuple(k.c_str()),
> > +						   std::forward_as_tuple(*polynomial));
> > +
> > +			it->second.setReferenceImageSize(descriptor.sensorSize);
> > +		}
> > +
> > +		auto [it, inserted] = lscData_.emplace(ct, components);
> > +		if (!inserted) {
> > +			LOG(LscPolynomial, Error)
> > +				<< "Multiple sets found for "
> > +				<< "color temperature " << ct;
> > +			return -EINVAL;
> > +		}
> > +	}
> > +
> > +	if (lscData_.empty()) {
> > +		LOG(LscPolynomial, Error) << "Failed to load any sets";
> > +		return -EINVAL;
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * \var LscPolynomialBase::lscData_
> > + * \brief The polynomial lsc data
> > + *
> > + * Maps colour temperatures to per-colour radial polynomial definitions.
> > + */
> > +
> > +/**
> > + * \class LscPolynomial
> > + * \brief Radial Polynomial lsc algorithm implementation
> > + *
> > + * Polynomial-based lsc algorithm implementation. The LscPolynomial class
> > + * implements lsc support using Polynomial to represent the shading artifacts
> > + * map.
> > + *
> > + * \sa LscImplementation
> > + * \sa LscAlgorithm
> > + *
> > + */
> > -} // namespace ipa
> > -} // namespace libcamera
> > +} /* namespace ipa */
> > +} /* namespace libcamera */
> > diff --git a/src/ipa/libipa/lsc_polynomial.h b/src/ipa/libipa/lsc_polynomial.h
> > index d7d9ae42e360..72cca591f260 100644
> > --- a/src/ipa/libipa/lsc_polynomial.h
> > +++ b/src/ipa/libipa/lsc_polynomial.h
> > @@ -1,15 +1,17 @@
> >   /* SPDX-License-Identifier: LGPL-2.1-or-later */
> >   /*
> > - * Copyright (C) 2024, Ideas On Board
> > + * Copyright (C) 2026, Ideas On Board
> >    *
> > - * Helper for radial polynomial used in lens shading correction.
> > + * Polynomial based lens shading correction
> >    */
> > +
> >   #pragma once
> >   #include <algorithm>
> >   #include <array>
> > -#include <assert.h>
> >   #include <cmath>
> > +#include <optional>
> > +#include <tuple>
> >   #include <libcamera/base/log.h>
> >   #include <libcamera/base/span.h>
> > @@ -18,64 +20,117 @@
> >   #include "libcamera/internal/value_node.h"
> > +#include "lsc_base.h"
> > +
> >   namespace libcamera {
> >   LOG_DECLARE_CATEGORY(LscPolynomial)
> >   namespace ipa {
> > -class LscPolynomial
> > +class Polynomial
> >   {
> >   public:
> > -	LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > -		      double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> > -		      double k4 = 0.0)
> > +	Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > +		   double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> > +		   double k4 = 0.0)
> >   		: cx_(cx), cy_(cy), cnx_(0), cny_(0),
> >   		  coefficients_({ k0, k1, k2, k3, k4 })
> >   	{
> >   	}
> > -	double sampleAtNormalizedPixelPos(double x, double y) const
> > +	Polynomial(const Polynomial &other) = default;
> > +
> > +	double sampleAtNormalizedPixelPos(double x, double y) const;
> > +	double getM() const;
> > +	void setReferenceImageSize(const Size &size);
> > +
> > +private:
> > +	double cx_;
> > +	double cy_;
> > +	double cnx_;
> > +	double cny_;
> > +	std::array<double, 5> coefficients_;
> > +	Size imageSize_;
> > +};
> > +
> > +class LscPolynomialBase
> > +{
> > +private:
> > +	using Components = std::map<std::string, Polynomial>;
> > +	using ComponentsMap = std::map<unsigned int, Components>;
> > +
> > +public:
> > +	int parseLscData(const ValueNode &yamlSets,
> > +			 const LscDescriptor &descriptor);
> > +
> > +protected:
> > +	ComponentsMap lscData_;
> > +};
> > +
> > +template<typename T, typename U>
> > +class LscPolynomial : public LscPolynomialBase, public LscImplementation<T, U>
> > +{
> > +public:
> > +	~LscPolynomial() {}
> > +
> > +	int parseLscData(const ValueNode &yamlSets,
> > +			 const LscDescriptor &descriptor) override
> >   	{
> > -		double dx = x - cnx_;
> > -		double dy = y - cny_;
> > -		double r = sqrt(dx * dx + dy * dy);
> > -		double res = 1.0;
> > -		for (unsigned int i = 0; i < coefficients_.size(); i++) {
> > -			res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> > -		}
> > -		return res;
> > +		return LscPolynomialBase::parseLscData(yamlSets, descriptor);
> >   	}
> > -	double getM() const
> > +	lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> > +					      const std::vector<double> &xPos,
> > +					      const std::vector<double> &yPos) override
> >   	{
> > -		double cpx = imageSize_.width * cx_;
> > -		double cpy = imageSize_.height * cy_;
> > -		double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
> > -		double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
> > +		lsc::ComponentsMap<T> components;
> > -		return sqrt(mx * mx + my * my);
> > -	}
> > +		for (auto &[t, c] : lscData_) {
> > +			lsc::Components<T> comp;
> > -	void setReferenceImageSize(const Size &size)
> > -	{
> > -		assert(!size.isNull());
> > -		imageSize_ = size;
> > +			for (auto &[k, p] : c) {
> > +				comp.emplace(std::piecewise_construct,
> > +					     std::forward_as_tuple(k),
> > +					     std::forward_as_tuple(samplePolynomial(p, xPos, yPos,
> > +										    cropRectangle)));
> > +			}
> > -		/* Calculate normalized centers */
> > -		double m = getM();
> > -		cnx_ = (size.width * cx_) / m;
> > -		cny_ = (size.height * cy_) / m;
> > +			components[t] = comp;
> > +		}
> > +
> > +		return components;
> >   	}
> >   private:
> > -	double cx_;
> > -	double cy_;
> > -	double cnx_;
> > -	double cny_;
> > -	std::array<double, 5> coefficients_;
> > +	std::vector<T> samplePolynomial(const Polynomial &poly,
> > +					Span<const double> xPositions,
> > +					Span<const double> yPositions,
> > +					const Rectangle &cropRectangle)
> > -	Size imageSize_;
> > +	{
> > +		double m = poly.getM();
> > +		double x0 = cropRectangle.x / m;
> > +		double y0 = cropRectangle.y / m;
> > +		double w = cropRectangle.width / m;
> > +		double h = cropRectangle.height / m;
> > +		std::vector<T> samples;
> > +
> > +		samples.reserve(xPositions.size() * yPositions.size());
> > +
> > +		for (double y : yPositions) {
> > +			for (double x : xPositions) {
> > +				double xp = x0 + x * w;
> > +				double yp = y0 + y * h;
> > +				float sample = static_cast<float>
> > +						(poly.sampleAtNormalizedPixelPos(xp, yp));
> > +
> > +				samples.push_back(U(sample).quantized());
> > +			}
> > +		}
> > +
> > +		return samples;
> > +	}
> >   };
> >   } /* namespace ipa */
> > @@ -83,8 +138,8 @@ private:
> >   #ifndef __DOXYGEN__
> >   template<>
> > -struct ValueNode::Accessor<ipa::LscPolynomial> {
> > -	std::optional<ipa::LscPolynomial> get(const ValueNode &obj) const
> > +struct ValueNode::Accessor<ipa::Polynomial> {
> > +	std::optional<ipa::Polynomial> get(const ValueNode &obj) const
> >   	{
> >   		std::optional<double> cx = obj["cx"].get<double>();
> >   		std::optional<double> cy = obj["cy"].get<double>();
> > @@ -98,7 +153,7 @@ struct ValueNode::Accessor<ipa::LscPolynomial> {
> >   			LOG(LscPolynomial, Error)
> >   				<< "Polynomial is missing a parameter";
> > -		return ipa::LscPolynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);
> > +		return ipa::Polynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);
> >   	}
> >   };
> > diff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build
> > index edf8eabd8b78..da6ea0c5e130 100644
> > --- a/src/ipa/libipa/meson.build
> > +++ b/src/ipa/libipa/meson.build
> > @@ -14,6 +14,9 @@ libipa_headers = files([
> >       'fixedpoint.h',
> >       'histogram.h',
> >       'interpolator.h',
> > +    'lsc.h',
> > +    'lsc_base.h',
> > +    'lsc_grid.h',
> >       'lsc_polynomial.h',
> >       'lux.h',
> >       'module.h',
> > @@ -36,6 +39,8 @@ libipa_sources = files([
> >       'fixedpoint.cpp',
> >       'histogram.cpp',
> >       'interpolator.cpp',
> > +    'lsc.cpp',
> > +    'lsc_grid.cpp',
> >       'lsc_polynomial.cpp',
> >       'lux.cpp',
> >       'module.cpp',
> > diff --git a/src/ipa/rkisp1/algorithms/lsc.cpp b/src/ipa/rkisp1/algorithms/lsc.cpp
> > index 25f7c512b35c..51076737ffa5 100644
> > --- a/src/ipa/rkisp1/algorithms/lsc.cpp
> > +++ b/src/ipa/rkisp1/algorithms/lsc.cpp
> > @@ -2,7 +2,7 @@
> >   /*
> >    * Copyright (C) 2021-2022, Ideas On Board
> >    *
> > - * RkISP1 Lens Shading Correction control
> > + * RkISP1 Lens Shading Correction algorithm
> >    */
> >   #include "lsc.h"
> > @@ -14,56 +14,17 @@
> >   #include <libcamera/base/log.h>
> >   #include <libcamera/base/utils.h>
> > -#include "libcamera/internal/value_node.h"
> > -
> > -#include "libipa/lsc_polynomial.h"
> > -#include "linux/rkisp1-config.h"
> > -
> >   /**
> >    * \file lsc.h
> >    */
> >   namespace libcamera {
> > -namespace ipa {
> > -
> > -template<typename T>
> > -void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,
> > -		       std::vector<T> &dest, double lambda)
> > -{
> > -	ASSERT(a.size() == b.size());
> > -	dest.resize(a.size());
> > -	for (size_t i = 0; i < a.size(); i++)
> > -		dest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;
> > -}
> > -
> > -template<>
> > -void Interpolator<rkisp1::algorithms::LensShadingCorrection::Components>::
> > -	interpolate(const rkisp1::algorithms::LensShadingCorrection::Components &a,
> > -		    const rkisp1::algorithms::LensShadingCorrection::Components &b,
> > -		    rkisp1::algorithms::LensShadingCorrection::Components &dest,
> > -		    double lambda)
> > -{
> > -	interpolateVector(a.r, b.r, dest.r, lambda);
> > -	interpolateVector(a.gr, b.gr, dest.gr, lambda);
> > -	interpolateVector(a.gb, b.gb, dest.gb, lambda);
> > -	interpolateVector(a.b, b.b, dest.b, lambda);
> > -}
> > -
> > -} /* namespace ipa */
> > -
> >   namespace ipa::rkisp1::algorithms {
> >   /**
> >    * \class LensShadingCorrection
> >    * \brief RkISP1 Lens Shading Correction control
> > - *
> > - * Due to the optical characteristics of the lens, the light intensity received
> > - * by the sensor is not uniform.
> > - *
> > - * The Lens Shading Correction algorithm applies multipliers to all pixels
> > - * to compensate for the lens shading effect. The coefficients are
> > - * specified in a downscaled table in the YAML tuning file.
> >    */
> >   LOG_DEFINE_CATEGORY(RkISP1Lsc)
> > @@ -72,265 +33,20 @@ namespace {
> >   constexpr int kColourTemperatureQuantization = 10;
> > -class LscPolynomialShadingDescriptor : public LensShadingCorrection::ShadingDescriptor
> > -{
> > -public:
> > -	LscPolynomialShadingDescriptor(const LscPolynomial &pr, const LscPolynomial &pgr,
> > -				       const LscPolynomial &pgb, const LscPolynomial &pb)
> > -		: pr_(pr), pgr_(pgr), pgb_(pgb), pb_(pb)
> > -	{
> > -	}
> > -
> > -	LensShadingCorrection::Components sampleForCrop(const Rectangle &cropRectangle,
> > -							Span<const double> xSizes,
> > -							Span<const double> ySizes) override;
> > -
> > -private:
> > -	std::vector<uint16_t> samplePolynomial(const LscPolynomial &poly,
> > -					       Span<const double> xPositions,
> > -					       Span<const double> yPositions,
> > -					       const Rectangle &cropRectangle);
> > -
> > -	std::vector<double> sizesListToPositions(Span<const double> sizes);
> > -
> > -	LscPolynomial pr_;
> > -	LscPolynomial pgr_;
> > -	LscPolynomial pgb_;
> > -	LscPolynomial pb_;
> > -};
> > -
> > -LensShadingCorrection::Components
> > -LscPolynomialShadingDescriptor::sampleForCrop(const Rectangle &cropRectangle,
> > -					      Span<const double> xSizes,
> > -					      Span<const double> ySizes)
> > -{
> > -	std::vector<double> xPos = sizesListToPositions(xSizes);
> > -	std::vector<double> yPos = sizesListToPositions(ySizes);
> > -
> > -	return {
> > -		.r = samplePolynomial(pr_, xPos, yPos, cropRectangle),
> > -		.gr = samplePolynomial(pgr_, xPos, yPos, cropRectangle),
> > -		.gb = samplePolynomial(pgb_, xPos, yPos, cropRectangle),
> > -		.b = samplePolynomial(pb_, xPos, yPos, cropRectangle)
> > -	};
> > -}
> > -
> > -std::vector<uint16_t>
> > -LscPolynomialShadingDescriptor::samplePolynomial(const LscPolynomial &poly,
> > -						 Span<const double> xPositions,
> > -						 Span<const double> yPositions,
> > -						 const Rectangle &cropRectangle)
> > -{
> > -	double m = poly.getM();
> > -	double x0 = cropRectangle.x / m;
> > -	double y0 = cropRectangle.y / m;
> > -	double w = cropRectangle.width / m;
> > -	double h = cropRectangle.height / m;
> > -	std::vector<uint16_t> samples;
> > -
> > -	samples.reserve(xPositions.size() * yPositions.size());
> > -
> > -	for (double y : yPositions) {
> > -		for (double x : xPositions) {
> > -			double xp = x0 + x * w;
> > -			double yp = y0 + y * h;
> > -			/*
> > -			 * The hardware uses 2.10 fixed point format and limits
> > -			 * the legal values to [1..3.999]. Scale and clamp the
> > -			 * sampled value accordingly.
> > -			 */
> > -			int v = static_cast<int>(
> > -				poly.sampleAtNormalizedPixelPos(xp, yp) *
> > -				1024);
> > -			v = std::clamp(v, 1024, 4095);
> > -			samples.push_back(v);
> > -		}
> > -	}
> > -	return samples;
> > -}
> > -
> > -/*
> > - * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left
> > - * quadrant of the grid. This is then mirrored in hardware to the other
> > - * quadrants. See parseSizes() for further details. For easier handling, this
> > - * function converts the cell sizes of half the grid to a list of position of
> > - * the whole grid (on one axis). Example:
> > - *
> > - * input:   | 0.2 | 0.3 |
> > - * output: 0.0   0.2   0.5   0.8   1.0
> > - */
> > -std::vector<double>
> > -LscPolynomialShadingDescriptor::sizesListToPositions(Span<const double> sizes)
> > -{
> > -	const int half = sizes.size();
> > -	std::vector<double> positions(half * 2 + 1);
> > -	double x = 0.0;
> > -
> > -	positions[half] = 0.5;
> > -	for (int i = 1; i <= half; i++) {
> > -		x += sizes[half - i];
> > -		positions[half - i] = 0.5 - x;
> > -		positions[half + i] = 0.5 + x;
> > -	}
> > -
> > -	return positions;
> > -}
> > -
> > -class LscPolynomialLoader
> > -{
> > -public:
> > -	LscPolynomialLoader(const Size &sensorSize)
> > -		: sensorSize_(sensorSize)
> > -	{
> > -	}
> > -
> > -	int parseLscData(const ValueNode &yamlSets,
> > -			 LensShadingCorrection::ShadingDescriptorMap &lscData);
> > -
> > -private:
> > -	Size sensorSize_;
> > -};
> > -
> > -int LscPolynomialLoader::parseLscData(const ValueNode &yamlSets,
> > -				      LensShadingCorrection::ShadingDescriptorMap &lscData)
> > +unsigned int quantize(unsigned int value, unsigned int step)
>
> I ended up using this in the IPU3 implementation too; perhaps it can move
> into the LscAlgorithm somewhere...though that can happen on-top of course.
>
> That's my last comment. It's really nice; implementing the IPU3 lens shading
> algorithm was a real breeze using this, so I think it works very very well
> at the goal of reducing the amount of duplicated code between the IPAs. I
> think we should settle the question above of whether to have "raw" gain or
> quantized gains (which are IPA module specific) in the tuning files and then
> this is good to go.
>

I wouldn't do that right now, but rather on top, once all
implementations have moved to use LscAlgorithm so we can change the
tuning file format in one go ?

>
> >   {
> > -	const auto &sets = yamlSets.asList();
> > -	for (const auto &yamlSet : sets) {
> > -		std::optional<LscPolynomial> pr, pgr, pgb, pb;
> > -		uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> > -
> > -		if (lscData.count(ct)) {
> > -			LOG(RkISP1Lsc, Error)
> > -				<< "Multiple sets found for "
> > -				<< "color temperature " << ct;
> > -			return -EINVAL;
> > -		}
> > -
> > -		pr = yamlSet["r"].get<LscPolynomial>();
> > -		pgr = yamlSet["gr"].get<LscPolynomial>();
> > -		pgb = yamlSet["gb"].get<LscPolynomial>();
> > -		pb = yamlSet["b"].get<LscPolynomial>();
> > -
> > -		if (!(pr || pgr || pgb || pb)) {
> > -			LOG(RkISP1Lsc, Error)
> > -				<< "Failed to parse polynomial for "
> > -				<< "colour temperature " << ct;
> > -			return -EINVAL;
> > -		}
> > -
> > -		pr->setReferenceImageSize(sensorSize_);
> > -		pgr->setReferenceImageSize(sensorSize_);
> > -		pgb->setReferenceImageSize(sensorSize_);
> > -		pb->setReferenceImageSize(sensorSize_);
> > -
> > -		lscData.emplace(
> > -			ct, std::make_unique<LscPolynomialShadingDescriptor>(
> > -				    *pr, *pgr, *pgb, *pb));
> > -	}
> > -
> > -	if (lscData.empty()) {
> > -		LOG(RkISP1Lsc, Error) << "Failed to load any sets";
> > -		return -EINVAL;
> > -	}
> > -
> > -	return 0;
> > +	return std::lround(value / static_cast<double>(step)) * step;
> >   }
> > -class LscTableShadingDescriptor : public LensShadingCorrection::ShadingDescriptor
> > -{
> > -public:
> > -	LscTableShadingDescriptor(LensShadingCorrection::Components components)
> > -		: lscData_(std::move(components))
> > -	{
> > -	}
> > -
> > -	LensShadingCorrection::Components
> > -	sampleForCrop([[maybe_unused]] const Rectangle &cropRectangle,
> > -		      [[maybe_unused]] Span<const double> xSizes,
> > -		      [[maybe_unused]] Span<const double> ySizes) override
> > -	{
> > -		LOG(RkISP1Lsc, Warning)
> > -			<< "Tabular LSC data doesn't support resampling";
> > -		return lscData_;
> > -	}
> > -
> > -private:
> > -	LensShadingCorrection::Components lscData_;
> > -};
> > -
> > -class LscTableLoader
> > -{
> > -public:
> > -	int parseLscData(const ValueNode &yamlSets,
> > -			 LensShadingCorrection::ShadingDescriptorMap &lscData);
> > -
> > -private:
> > -	std::vector<uint16_t> parseTable(const ValueNode &tuningData,
> > -					 const char *prop);
> > -};
> > -
> > -int LscTableLoader::parseLscData(const ValueNode &yamlSets,
> > -				 LensShadingCorrection::ShadingDescriptorMap &lscData)
> > -{
> > -	const auto &sets = yamlSets.asList();
> > -
> > -	for (const auto &yamlSet : sets) {
> > -		uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> > -
> > -		if (lscData.count(ct)) {
> > -			LOG(RkISP1Lsc, Error)
> > -				<< "Multiple sets found for color temperature "
> > -				<< ct;
> > -			return -EINVAL;
> > -		}
> > -
> > -		LensShadingCorrection::Components set;
> > -		set.r = parseTable(yamlSet, "r");
> > -		set.gr = parseTable(yamlSet, "gr");
> > -		set.gb = parseTable(yamlSet, "gb");
> > -		set.b = parseTable(yamlSet, "b");
> > -
> > -		if (set.r.empty() || set.gr.empty() ||
> > -		    set.gb.empty() || set.b.empty()) {
> > -			LOG(RkISP1Lsc, Error)
> > -				<< "Set for color temperature " << ct
> > -				<< " is missing tables";
> > -			return -EINVAL;
> > -		}
> > -
> > -		lscData.emplace(
> > -			ct, std::make_unique<LscTableShadingDescriptor>(std::move(set)));
> > -	}
> > -
> > -	if (lscData.empty()) {
> > -		LOG(RkISP1Lsc, Error) << "Failed to load any sets";
> > -		return -EINVAL;
> > -	}
> > -
> > -	return 0;
> > -}
> > +} /* namespace */
> > -std::vector<uint16_t> LscTableLoader::parseTable(const ValueNode &tuningData,
> > -						 const char *prop)
> > +LensShadingCorrection::LensShadingCorrection()
> > +	: lastAppliedCt_(0), lastAppliedQuantizedCt_(0)
> >   {
> > -	static constexpr unsigned int kLscNumSamples =
> > -		RKISP1_CIF_ISP_LSC_SAMPLES_MAX * RKISP1_CIF_ISP_LSC_SAMPLES_MAX;
> > -
> > -	std::vector<uint16_t> table =
> > -		tuningData[prop].get<std::vector<uint16_t>>().value_or(utils::defopt);
> > -	if (table.size() != kLscNumSamples) {
> > -		LOG(RkISP1Lsc, Error)
> > -			<< "Invalid '" << prop << "' values: expected "
> > -			<< kLscNumSamples
> > -			<< " elements, got " << table.size();
> > -		return {};
> > -	}
> > -
> > -	return table;
> >   }
> > -std::vector<double> parseSizes(const ValueNode &tuningData,
> > -			       const char *prop)
> > +std::vector<double> LensShadingCorrection::parseSizes(const ValueNode &tuningData,
> > +						      const char *prop)
> >   {
> >   	std::vector<double> sizes =
> >   		tuningData[prop].get<std::vector<double>>().value_or(utils::defopt);
> > @@ -359,22 +75,36 @@ std::vector<double> parseSizes(const ValueNode &tuningData,
> >   	return sizes;
> >   }
> > -unsigned int quantize(unsigned int value, unsigned int step)
> > +/*
> > + * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left
> > + * quadrant of the grid. This is then mirrored in hardware to the other
> > + * quadrants. See parseSizes() for further details. For easier handling, this
> > + * function converts the cell sizes of half the grid to a list of position of
> > + * the whole grid (on one axis). Example:
> > + *
> > + * input:   | 0.2 | 0.3 |
> > + * output: 0.0   0.2   0.5   0.8   1.0
> > + */
> > +std::vector<double> LensShadingCorrection::sizesToPositions(Span<const double> sizes)
> >   {
> > -	return std::lround(value / static_cast<double>(step)) * step;
> > -}
> > +	const int half = sizes.size();
> > +	std::vector<double> positions(half * 2 + 1);
> > +	double x = 0.0;
> > -} /* namespace */
> > +	positions[half] = 0.5;
> > +	for (int i = 1; i <= half; i++) {
> > +		x += sizes[half - i];
> > +		positions[half - i] = 0.5 - x;
> > +		positions[half + i] = 0.5 + x;
> > +	}
> > -LensShadingCorrection::LensShadingCorrection()
> > -	: lastAppliedCt_(0), lastAppliedQuantizedCt_(0)
> > -{
> > +	return positions;
> >   }
> >   /**
> >    * \copydoc libcamera::ipa::Algorithm::init
> >    */
> > -int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
> > +int LensShadingCorrection::init(IPAContext &context,
> >   				const ValueNode &tuningData)
> >   {
> >   	xSize_ = parseSizes(tuningData, "x-size");
> > @@ -383,56 +113,27 @@ int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
> >   	if (xSize_.empty() || ySize_.empty())
> >   		return -EINVAL;
> > -	/* Get all defined sets to apply. */
> > -	const ValueNode &yamlSets = tuningData["sets"];
> > -	if (!yamlSets.isList()) {
> > -		LOG(RkISP1Lsc, Error)
> > -			<< "'sets' parameter not found in tuning file";
> > -		return -EINVAL;
> > -	}
> > -
> > -	ShadingDescriptorMap lscData;
> > -	int ret = 0;
> > -
> > -	std::string type = tuningData["type"].get<std::string>("table");
> > -	if (type == "table") {
> > -		LOG(RkISP1Lsc, Debug) << "Loading tabular LSC data.";
> > -		auto loader = LscTableLoader();
> > -		ret = loader.parseLscData(yamlSets, lscData);
> > -	} else if (type == "polynomial") {
> > -		LOG(RkISP1Lsc, Debug) << "Loading polynomial LSC data.";
> > -		/*
> > -		 * \todo: Most likely the reference frame should be native_size.
> > -		 * Let's wait how the internal discussions progress.
> > -		 */
> > -		auto loader = LscPolynomialLoader(context.sensorInfo.activeAreaSize);
> > -		ret = loader.parseLscData(yamlSets, lscData);
> > -	} else {
> > -		LOG(RkISP1Lsc, Error) << "Unsupported LSC data type '"
> > -				      << type << "'";
> > -		ret = -EINVAL;
> > -	}
> > -
> > -	if (ret)
> > -		return ret;
> > +	xPos_ = sizesToPositions(xSize_);
> > +	yPos_ = sizesToPositions(ySize_);
> > -	context.ctrlMap[&controls::LensShadingCorrectionEnable] =
> > -		ControlInfo(false, true, true);
> > -
> > -	shadingDescriptors_ = std::move(lscData);
> > -
> > -	return 0;
> > +	return lscAlgo_.init(tuningData, context.ctrlMap, {
> > +				.keys = { "r", "gr", "gb", "b" },
> > +				.numHCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,
> > +				.numVCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,
> > +				.sensorSize = context.sensorInfo.activeAreaSize
> > +			     });
> >   }
> >   /**
> >    * \copydoc libcamera::ipa::Algorithm::configure
> >    */
> >   int LensShadingCorrection::configure(IPAContext &context,
> > -				     [[maybe_unused]] const IPACameraSensorInfo &configInfo)
> > +				     const IPACameraSensorInfo &configInfo)
> >   {
> >   	const Size &size = context.configuration.sensor.size;
> >   	Size totalSize{};
> > +	/* Calculate gradients. */
> >   	for (unsigned int i = 0; i < RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE; ++i) {
> >   		xSizes_[i] = xSize_[i] * size.width;
> >   		ySizes_[i] = ySize_[i] * size.height;
> > @@ -455,16 +156,8 @@ int LensShadingCorrection::configure(IPAContext &context,
> >   		yGrad_[i] = std::round(32768 / ySizes_[i]);
> >   	}
> > -	LOG(RkISP1Lsc, Debug) << "Sample LSC data for " << configInfo.analogCrop;
> > -	std::map<unsigned int, LensShadingCorrection::Components> shadingData;
> > -	for (const auto &[t, descriptor] : shadingDescriptors_)
> > -		shadingData[t] = descriptor->sampleForCrop(configInfo.analogCrop,
> > -							   xSize_, ySize_);
> > -
> > -	sets_.setData(std::move(shadingData));
> > -
> > -	context.activeState.lsc.enabled = true;
> > -	return 0;
> > +	return lscAlgo_.configure(context.activeState.lsc, configInfo.analogCrop,
> > +				  xPos_, yPos_);
> >   }
> >   void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)
> > @@ -476,12 +169,16 @@ void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)
> >   }
> >   void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
> > -				      const Components &set)
> > +				      const ipa::lsc::Components<uint16_t> &set)
> >   {
> > -	std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
> > -	std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
> > -	std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
> > -	std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
> > +	const auto &r = set.at("r");
> > +	std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
> > +	const auto &gr = set.at("gr");
> > +	std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
> > +	const auto &gb = set.at("gb");
> > +	std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
> > +	const auto &b = set.at("b");
> > +	std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
> >   }
> >   /**
> > @@ -492,19 +189,8 @@ void LensShadingCorrection::queueRequest(IPAContext &context,
> >   					 IPAFrameContext &frameContext,
> >   					 const ControlList &controls)
> >   {
> > -	auto &lsc = context.activeState.lsc;
> > -
> > -	const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);
> > -	if (lscEnable && *lscEnable != lsc.enabled) {
> > -		lsc.enabled = *lscEnable;
> > -
> > -		LOG(RkISP1Lsc, Debug)
> > -			<< (lsc.enabled ? "Enabling" : "Disabling") << " Lsc";
> > -
> > -		frameContext.lsc.update = true;
> > -	}
> > -
> > -	frameContext.lsc.enabled = lsc.enabled;
> > +	lscAlgo_.queueRequest(context.activeState.lsc, frameContext.lsc,
> > +			      controls);
> >   }
> >   /**
> > @@ -542,7 +228,7 @@ void LensShadingCorrection::prepare([[maybe_unused]] IPAContext &context,
> >   	setParameters(*config);
> > -	const Components &set = sets_.getInterpolated(quantizedCt);
> > +	const auto &set = lscAlgo_.interpolateComponents(quantizedCt);
> >   	copyTable(*config, set);
> >   	lastAppliedCt_ = ct;
> > @@ -562,7 +248,7 @@ void LensShadingCorrection::process([[maybe_unused]] IPAContext &context,
> >   				    [[maybe_unused]] const rkisp1_stat_buffer *stats,
> >   				    ControlList &metadata)
> >   {
> > -	metadata.set(controls::LensShadingCorrectionEnable, frameContext.lsc.enabled);
> > +	lscAlgo_.process(frameContext.lsc, metadata);
> >   }
> >   REGISTER_IPA_ALGORITHM(LensShadingCorrection, "LensShadingCorrection")
> > diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
> > index 0a256e225327..7c656d95acfb 100644
> > --- a/src/ipa/rkisp1/algorithms/lsc.h
> > +++ b/src/ipa/rkisp1/algorithms/lsc.h
> > @@ -2,17 +2,23 @@
> >   /*
> >    * Copyright (C) 2021-2022, Ideas On Board
> >    *
> > - * RkISP1 Lens Shading Correction control
> > + * RkISP1 Lens Shading Correction algorithm
> >    */
> >   #pragma once
> > -#include <map>
> > -#include <memory>
> > +#include <vector>
> > -#include "libipa/interpolator.h"
> > +#include <linux/rkisp1-config.h>
> > +
> > +#include "libcamera/internal/value_node.h"
> > +
> > +#include "libipa/fixedpoint.h"
> > +#include "libipa/lsc.h"
> >   #include "algorithm.h"
> > +#include "ipa_context.h"
> > +#include "params.h"
> >   namespace libcamera {
> > @@ -37,39 +43,28 @@ public:
> >   		     const rkisp1_stat_buffer *stats,
> >   		     ControlList &metadata) override;
> > -	struct Components {
> > -		std::vector<uint16_t> r;
> > -		std::vector<uint16_t> gr;
> > -		std::vector<uint16_t> gb;
> > -		std::vector<uint16_t> b;
> > -	};
> > -
> > -	class ShadingDescriptor
> > -	{
> > -	public:
> > -		virtual ~ShadingDescriptor() = default;
> > -		virtual Components sampleForCrop(const Rectangle &cropRectangle,
> > -						 Span<const double> xSizes,
> > -						 Span<const double> ySizes) = 0;
> > -	};
> > -
> > -	using ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;
> > -
> >   private:
> > +	std::vector<double> parseSizes(const ValueNode &tuningData,
> > +				       const char *prop);
> > +	std::vector<double> sizesToPositions(Span<const double> sizes);
> > +
> >   	void setParameters(rkisp1_cif_isp_lsc_config &config);
> > -	void copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);
> > +	void copyTable(rkisp1_cif_isp_lsc_config &config,
> > +		       const ipa::lsc::Components<uint16_t> &set0);
> > -	ShadingDescriptorMap shadingDescriptors_;
> > -	ipa::Interpolator<Components> sets_;
> >   	std::vector<double> xSize_;
> >   	std::vector<double> ySize_;
> >   	uint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> >   	uint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> >   	uint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> >   	uint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > +	std::vector<double> xPos_;
> > +	std::vector<double> yPos_;
> >   	unsigned int lastAppliedCt_;
> >   	unsigned int lastAppliedQuantizedCt_;
> > +
> > +	LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;
> >   };
> >   } /* namespace ipa::rkisp1::algorithms */
> > diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
> > index cd97e10bcf2b..005f4102b4f6 100644
> > --- a/src/ipa/rkisp1/ipa_context.h
> > +++ b/src/ipa/rkisp1/ipa_context.h
> > @@ -30,6 +30,7 @@
> >   #include "libipa/ccm.h"
> >   #include "libipa/fc_queue.h"
> >   #include "libipa/fixedpoint.h"
> > +#include "libipa/lsc.h"
> >   namespace libcamera {
> > @@ -138,9 +139,7 @@ struct IPAActiveState {
> >   		double strength;
> >   	} wdr;
> > -	struct {
> > -		bool enabled;
> > -	} lsc;
> > +	ipa::lsc::ActiveState lsc;
> >   };
> >   struct IPAFrameContext : public FrameContext {
> > @@ -213,10 +212,7 @@ struct IPAFrameContext : public FrameContext {
> >   		double gain;
> >   	} wdr;
> > -	struct {
> > -		bool enabled;
> > -		bool update;
> > -	} lsc;
> > +	ipa::lsc::FrameContext lsc;
> >   };
> >   struct IPAContext {
> >
>
Dan Scally June 23, 2026, 1:36 p.m. UTC | #4
Hi Jacopo

On 23/06/2026 13:39, Jacopo Mondi wrote:
> Hi Dan
> 
> On Tue, Jun 23, 2026 at 10:30:40AM +0100, Dan Scally wrote:
>> Morning Jacopo
>>
>> On 15/06/2026 15:05, Jacopo Mondi wrote:
>>> Introduce an Lsc algorithm implementation in libIPA.
>>>
>>> The LscAlgorithm class supports two algorithms: grid-based lsc and
>>> polynomial-based lsc.
>>>
>>> Both implementations have been generalized from the existing ones in the
>>> RkISP1 IPA.
>>>
>>> Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
>>> ---
>>>    src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++
>>>    src/ipa/libipa/lsc.h              | 145 +++++++++++++
>>>    src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++
>>>    src/ipa/libipa/lsc_base.h         |  59 ++++++
>>>    src/ipa/libipa/lsc_grid.cpp       |  33 +++
>>>    src/ipa/libipa/lsc_grid.h         | 113 ++++++++++
>>>    src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-
>>>    src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----
>>>    src/ipa/libipa/meson.build        |   5 +
>>>    src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------
>>>    src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--
>>>    src/ipa/rkisp1/ipa_context.h      |  10 +-
>>>    12 files changed, 1126 insertions(+), 450 deletions(-)
>>>
>>> diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
>>> new file mode 100644
>>> index 000000000000..ae713a776778
>>> --- /dev/null
>>> +++ b/src/ipa/libipa/lsc.cpp
>>> @@ -0,0 +1,322 @@
>>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>>> +/*
>>> + * Copyright (C) 2026 Ideas on Board Oy
>>> + *
>>> + * libIPA Lsc algorithms
>>> + */
>>> +
>>> +#include "lsc.h"
>>> +
>>> +#include <libcamera/base/log.h>
>>> +
>>> +/**
>>> + * \file lsc.h
>>> + * \brief libipa lsc algorithm
>>> + */
>>> +
>>> +namespace libcamera {
>>> +
>>> +LOG_DEFINE_CATEGORY(Lsc)
>>> +
>>> +namespace ipa {
>>> +
>>> +namespace lsc {
>>> +
>>> +/**
>>> + * \struct ActiveState
>>> + * \brief The lsc active state
>>> + *
>>> + * \var ActiveState::enabled
>>> + * \brief Boolean flag for the LscAlgorithm enable status
>>> + */
>>> +
>>> +/**
>>> + * \struct FrameContext
>>> + * \brief The lsc frame context
>>> + *
>>> + * \var FrameContext::enabled
>>> + * \brief Boolean flag for the LscAlgorithm enable status
>>> + *
>>> + * \var FrameContext::update
>>> + * \brief Boolean flag for the LscAlgorithm updated status
>>> + */
>>> +
>>> +} /* namespace lsc */
>>> +
>>> +#ifndef __DOXYGEN__
>>> +template<>
>>> +void Interpolator<lsc::Components<uint16_t>>::
>>> +	interpolate(const lsc::Components<uint16_t> &a,
>>> +		    const lsc::Components<uint16_t> &b,
>>> +		    lsc::Components<uint16_t> &dest,
>>> +		    double lambda)
>>> +{
>>> +	for (auto const &[k, v] : a)
>>> +		interpolateVector(v, b.at(k), dest[k], lambda);
>>> +}
>>> +#endif
>>> +
>>> +/**
>>> + * \class LscAlgorithmBase
>>> + * \brief Base class for LscAlgorithm
>>> + *
>>> + * Base class for LscAlgorithm for non-templated functions implementation
>>> + */
>>> +
>>> +/**
>>> + * \brief Queue a request to the lsc algorithm
>>> + * \param[in] state The lsc active state
>>> + * \param[in] context The lsc frame context
>>> + * \param[in] controls The list of controls associated with a Request
>>> + *
>>> + * Queue a new list of \a controls to the lsc algorithm.
>>> + * The only supported control is controls::LensShadingCorrectionEnable.
>>> + */
>>> +void LscAlgorithmBase::queueRequest(lsc::ActiveState &state,
>>> +				    lsc::FrameContext &context,
>>> +				    const ControlList &controls)
>>> +{
>>> +	const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);
>>> +	if (lscEnable && *lscEnable != state.enabled) {
>>> +		state.enabled = *lscEnable;
>>> +
>>> +		LOG(Lsc, Debug)
>>> +			<< (state.enabled ? "Enabling" : "Disabling") << " Lsc";
>>> +
>>> +		context.update = true;
>>> +	}
>>> +
>>> +	context.enabled = state.enabled;
>>> +}
>>> +
>>> +/**
>>> + * \brief Populate the list of lsc metadata
>>> + * \param[in] context The lsc frame context
>>> + * \param[in] metadata The list of metadata
>>> + *
>>> + * Populates the list of \a metadata with controls handled by the LscAlgorithm
>>> + * class. The only supported metadata is controls::LensShadingCorrectionEnable.
>>> + */
>>> +void LscAlgorithmBase::process(lsc::FrameContext &context, ControlList &metadata)
>>> +{
>>> +	metadata.set(controls::LensShadingCorrectionEnable, context.enabled);
>>> +}
>>> +
>>> +/**
>>> + * \class LscAlgorithm
>>> + * \brief libIPA lsc algorithm implementation
>>> + * \tparam T The type used to store the gain values when loaded from tuning file
>>> + * \tparam U The fixedpoint lsc engine register format
>>> + *
>>> + * Due to the optical characteristics of the lens, the light intensity received
>>> + * by the sensor is not uniform. The Lens Shading Correction algorithm applies
>>> + * multipliers to all pixels to compensate for the lens shading effect.
>>> + *
>>> + * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.
>>> + * It provides support for parsing the tuning file content and generate tables
>>
>> s/generate/generates?
>>
> 
> ack
> 
>>> + * of per-colour temperature gains that IPA algorithms can use to program their
>>> + * lsc engine.
>>> + *
>>> + * The init() function parses the tuning file and loads the gain tables either
>>> + * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain
>>> + * tables are organized per-colour temperature with per-colour components gain
>>> + * vectors or polynomial coefficients. The colour components names are
>>> + * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc
>>> + * engine support 4 colour components (r, gr, gb, b), some only support 3 colour
>>> + * components (r, g, b). The name (and number) of the expected colour components
>>> + * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys
>>> + * field.
>>> + *
>>> + * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour
>>> + * components. The gain table has been here omitted, but the expected number
>>> + * of entries has be equal to
>>> + * LscDescriptor::numHCells * LscDescriptor::numVCells.
>>> + *
>>> + * \code{.yaml}
>>> + * - Lsc:
>>> + *    sets:
>>> + *      - ct: 2500
>>> + *        r: [
>>> + *        	.. gains table omitted..
>>> + *        ]
>>> + *        g: [
>>> + *        	.. gains table omitted..
>>> + *        ]
>>> + *        b: [
>>> + *        	.. gains table omitted..
>>> + *        ]
>>> + *      - ct: 6500
>>> + *        r: [
>>> + *        	.. gains table omitted..
>>> + *        ]
>>> + *        g: [
>>> + *        	.. gains table omitted..
>>> + *        ]
>>> + *        b: [
>>> + *        	.. gains table omitted..
>>> + *        ]
>>> + * \endcode
>>> + *
>>> + * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'
>>> + * colour components:
>>> + *
>>> + * \code{.yaml}
>>> + * - Lsc:
>>> + *    type: "polynomial"
>>> + *    sets:
>>> + *      - ct: 2500
>>> + *        r:
>>> + *          cx: 0.5006571711950275
>>> + *          cy: 0.510093737499277
>>> + *          k0: 1.5393282208428813
>>> + *          k1: -1.1434559757908016
>>> + *          k2: 4.332602305814554
>>> + *          k3: 0.0
>>> + *          k4: 0.0
>>> + *        gr:
>>> + *          cx: 0.5009320529087338
>>> + *          cy: 0.511208038949085
>>> + *          k0: 1.5634738574805407
>>> + *          k1: -1.5623484259968348
>>> + *          k2: 4.846686073656501
>>> + *          k3: 0.0
>>> + *          k4: 0.0
>>> + *        gb:
>>> + *          cx: 0.5012013290343839
>>> + *          cy: 0.5128251541578288
>>> + *          k0: 1.526147944919103
>>> + *          k1: -1.4316976083689723
>>> + *          k2: 4.792604063222728
>>> + *          k3: 0.0
>>> + *          k4: 0.0
>>> + *        b:
>>> + *          cx: 0.49864139511067784
>>> + *          cy: 0.5162095081739346
>>> + *          k0: 1.0405245474038738
>>> + *          k1: 0.05618339879447103
>>> + *          k2: 1.8792813594001752
>>> + *          k3: 0.0
>>> + *          k4: 0.0
>>> + *      - ct: 6000
>>> + *        r:
>>> + *          cx: 0.5006202239353942
>>> + *          cy: 0.5099531318307661
>>> + *          k0: 1.4702946023945032
>>> + *          k1: -0.8893767547927631
>>> + *          k2: 3.920547732201387
>>> + *          k3: 0.0
>>> + *          k4: 0.0
>>> + *        gr:
>>> + *          cx: 0.500907874178317
>>> + *          cy: 0.511084916024106
>>> + *          k0: 1.5336172760559457
>>> + *          k1: -1.39964026514435
>>> + *          k2: 4.565487728954618
>>> + *          k3: 0.0
>>> + *          k4: 0.0
>>> + *        gb:
>>> + *          cx: 0.5011898608900477
>>> + *          cy: 0.5126797906745105
>>> + *          k0: 1.5013145790354843
>>> + *          k1: -1.2747407173754124
>>> + *          k2: 4.514682876897286
>>> + *          k3: 0.0
>>> + *          k4: 0.0
>>> + *        b:
>>> + *          cx: 0.4987561413116136
>>> + *          cy: 0.5159619420778772
>>> + *          k0: 1.0102986422191802
>>> + *          k1: 0.13263449763985727
>>> + *          k2: 1.686556107316064
>>> + *          k3: 0.0
>>> + *          k4: 0.0
>>> + * \endcode
>>> + *
>>> + * The lsc tables or the polynomial definition are generated at tuning time
>>> + * using an image of known resolution which needs to be specified in
>>> + * LscDescriptor::sensorSize.
>>> + *
>>> + * At LscAlgorithm::configure() time the lsc tables are re-sampled on the
>>> + * sensor's crop rectangle in use to adapt them to the configuration in use for
>>> + * a streaming session. Polynomial lsc tables support re-sampling and can be
>>> + * applied to any sensor configuration. Grid-based lsc tables cannot be
>>> + * re-sampled and the configuration as parsed from the tuning file is used for
>>> + * all sensor configurations providing best-effort results.
>>> + *
>>> + * When the IPA algorithms wants to get access to the (resampled) tables to
>>> + * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get
>>> + * an lsc table interpolated by the LscAlgorithm class for the specified colour
>>> + * temperature. If the algorithm wants to access the non-interpolated tables it
>>> + * can retrieve them using LscAlgorithm::getComponents().
>>> + */
>>> +
>>> +/**
>>> + * \fn LscAlgorithm::init()
>>> + * \param[in] tuningData The tuning data
>>> + * \param[in] controls The IPA list of supported controls
>>> + * \param[in] descriptor The lsc engine descriptor
>>> + *
>>> + * Parse \a tuningData according to the settings specified in \a descriptor to
>>> + * populate the lsc data and registers lsc controls in \a controls.
>>> + *
>>> + * \return 0 on success, a negative error code otherwise
>>> + */
>>> +
>>> +/**
>>> + * \fn LscAlgorithm::configure()
>>> + * \param[in] state The lsc active state
>>> + * \param[in] analogCrop The current sensor analog crop rectangle
>>> + * \param[in] xPos List of horizontal positions of the LSC grid nodes
>>> + * \param[in] yPos List of vertical positions of the LSC grid nodes
>>
>> Can xPos and yPos go into init()? I think from the implementation details
>> that they're static rather than something determined by the configuration.
> 
> Can't xPos and yPos depend on the grid positioning on a new sensor
> crop rectangle ? Isn't it worth to allow to specify them at configure
> time ?
> 
> I know rkisp1 currently doesn't do that and feed the algorithms with
> the same grid, but as this is performed at configure() time I presume
> there was a use case envisioned for that ?

Ah, I don't know, I was just going off how the rkisp1 implementation currently does things. If it's 
something that can change with the configuration then here's the right place.

> 
> 
>>> + *
>>> + * Re-sample the lsc data for an \a analogCrop.
>>> + *
>>> + * Lsc data are generated at tuning time using a known sensor configuration.
>>> + * When a new streaming session is started, it might use a different sensor
>>> + * configuration for which the lsc tables need to be adjusted to.
>>> + *
>>> + * This function re-generates the lsc tables to adapt them to a new sensor
>>> + * configuration, specifically it re-samples the lsc data for a new \a
>>> + * analogCrop on a grid specified by \a xPos and \a yPos. Re-sampling of
>>> + * lsc data is only supported by polynomial-based lsc tables.
>>> + *
>>> + * \sa LscImplementation::resampleLscData
>>> + *
>>> + * \return 0 on success, a negative error code otherwise
>>> + */
>>> +
>>> +/**
>>> + * \fn LscAlgorithm::interpolateComponents
>>> + * \brief Interpolate the lsc tables for a given colour temperature
>>> + * \param[in] ct The colour temperature
>>> + *
>>> + * Lsc data are generated using different colour temperatures during the
>>> + * tuning phase.
>>> + *
>>> + * This function returns the interpolated lsc data for a given \a ct
>>> + * colour temperature.
>>> + *
>>> + * IPA algorithm can use this function to obtain a list of gains per-colour
>>> + * component to program their lsc engines with every time a significant enough
>>> + * change in colour temperature is detected.
>>> + *
>>> + * Calling this function is only valid after LscAlgorithm::configure() has been
>>> + * called. An empty components list is returned otherwise.
>>> + *
>>> + * \return The lsc gains table interpolated for temperature \a ct
>>> + */
>>> +
>>> +/**
>>> + * \fn LscAlgorithm::getComponents
>>> + *
>>> + * Return the map of lsc data per colour temperature.
>>> + *
>>> + * Calling this function is only valid after LscAlgorithm::configure() has been
>>> + * called. An empty components list is returned otherwise.
>>> + *
>>> + * \return The map of lsc gains tables per colour-temperature
>>> + */
>>> +
>>> +} /* namespace ipa */
>>> +
>>> +} /* namespace libcamera */
>>> diff --git a/src/ipa/libipa/lsc.h b/src/ipa/libipa/lsc.h
>>> new file mode 100644
>>> index 000000000000..8bfa3a7c4ecf
>>> --- /dev/null
>>> +++ b/src/ipa/libipa/lsc.h
>>> @@ -0,0 +1,145 @@
>>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>>> +/*
>>> + * Copyright (C) 2026 Ideas on Board Oy
>>> + *
>>> + * libIPA Lsc algorithms
>>> + */
>>> +
>>> +#pragma once
>>> +
>>> +#include <memory>
>>> +
>>> +#include <libcamera/control_ids.h>
>>> +
>>> +#include "interpolator.h"
>>> +#include "lsc_grid.h"
>>> +#include "lsc_polynomial.h"
>>> +
>>> +namespace libcamera {
>>> +
>>> +LOG_DECLARE_CATEGORY(Lsc)
>>> +
>>> +namespace ipa {
>>> +
>>> +namespace lsc {
>>> +
>>> +struct ActiveState {
>>> +	bool enabled;
>>> +};
>>> +
>>> +struct FrameContext {
>>> +	bool enabled;
>>> +	bool update;
>>> +};
>>> +
>>> +} /* namespace lsc */
>>> +
>>> +#ifndef __DOXYGEN__
>>> +template<typename T>
>>> +void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,
>>> +		       std::vector<T> &dest, double lambda)
>>> +{
>>> +	ASSERT(a.size() == b.size());
>>> +	dest.resize(a.size());
>>> +	for (size_t i = 0; i < a.size(); i++)
>>> +		dest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;
>>> +}
>>> +
>>> +template<>
>>> +void Interpolator<lsc::Components<uint16_t>>::
>>> +	interpolate(const lsc::Components<uint16_t> &a,
>>> +		    const lsc::Components<uint16_t> &b,
>>> +		    lsc::Components<uint16_t> &dest,
>>> +		    double lambda);
>>> +#endif /* __DOXYGEN__ */
>>> +
>>> +class LscAlgorithmBase
>>> +{
>>> +public:
>>> +	void queueRequest(lsc::ActiveState &state, lsc::FrameContext &context,
>>> +			  const ControlList &controls);
>>> +	void process(lsc::FrameContext &context, ControlList &metadata);
>>> +};
>>> +
>>> +template<typename T, typename U>
>>> +class LscAlgorithm : public LscAlgorithmBase
>>> +{
>>> +public:
>>> +	int init(const ValueNode &tuningData, ControlInfoMap::Map &controls,
>>> +		 LscDescriptor descriptor)
>>> +	{
>>> +		polynomial_ = false;
>>> +
>>> +		std::string type = tuningData["type"].get<std::string>("table");
>>> +		if (type == "table") {
>>> +			impl_ = std::make_unique<LscGrid<T, U>>();
>>> +			LOG(Lsc, Debug) << "Using grid-based Lsc";
>>> +		} else if (type == "polynomial") {
>>> +			impl_ = std::make_unique<LscPolynomial<T, U>>();
>>> +			polynomial_ = true;
>>> +			LOG(Lsc, Debug) << "Using polynomial Lsc";
>>> +		} else {
>>> +			LOG(Lsc, Error) << "Unsupported Lsc algorithm '"
>>> +					<< type << "'";
>>> +			return -EINVAL;
>>> +		}
>>> +
>>> +		const ValueNode &yamlSets = tuningData["sets"];
>>> +		if (!yamlSets.isList()) {
>>> +			LOG(Lsc, Error) << "'sets' parameter not found in tuning file";
>>> +			return -EINVAL;
>>> +		}
>>> +
>>> +		int ret = impl_->parseLscData(yamlSets, descriptor);
>>> +		if (ret)
>>> +			return ret;
>>> +
>>> +		controls[&controls::LensShadingCorrectionEnable] =
>>> +			ControlInfo(false, true, true);
>>> +
>>> +		return 0;
>>> +	}
>>> +
>>> +	int configure(lsc::ActiveState &state, const Rectangle &analogCrop,
>>> +		      const std::vector<double> &xPos,
>>> +		      const std::vector<double> &yPos)
>>> +	{
>>> +		LOG(Lsc, Debug) << "Sample Lsc data for " << analogCrop;
>>> +		lsc::ComponentsMap<T> lscData =
>>> +			impl_->resampleLscData(analogCrop, xPos, yPos);
>>> +
>>> +		/*
>>> +		 * Retain a copy of the components table.
>>> +		 *
>>> +		 * We could avoid a copy here if getComponents() could
>>> +		 * return sets_.data() but I wasn't able to work around the
>>> +		 * compiler refusing it.
>>> +		 */
>>> +		lscData_ = lscData;
>>> +
>>> +		sets_.setData(std::move(lscData));
>>> +		state.enabled = true;
>>> +
>>> +		return 0;
>>> +	}
>>> +
>>> +	const lsc::Components<T> interpolateComponents(unsigned int ct)
>>> +	{
>>> +		return sets_.getInterpolated(ct);
>>> +	}
>>> +
>>> +	const lsc::ComponentsMap<T> getComponents()
>>> +	{
>>> +		return lscData_;
>>> +	}
>>> +
>>> +private:
>>> +	std::unique_ptr<LscImplementation<T, U>> impl_;
>>> +	Interpolator<lsc::Components<T>> sets_;
>>> +	lsc::ComponentsMap<T> lscData_;
>>> +	bool polynomial_;
>>> +};
>>> +
>>> +} /* namespace ipa */
>>> +
>>> +} /* namespace libcamera */
>>> diff --git a/src/ipa/libipa/lsc_base.cpp b/src/ipa/libipa/lsc_base.cpp
>>> new file mode 100644
>>> index 000000000000..fba8d2f00d54
>>> --- /dev/null
>>> +++ b/src/ipa/libipa/lsc_base.cpp
>>> @@ -0,0 +1,152 @@
>>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>>> +/*
>>> + * Copyright (C) 2026 Ideas on Board Oy
>>> + *
>>> + * Base classes and types for LSC algorithms implementations
>>> + */
>>> +
>>> +#include "lsc_base.h"
>>> +
>>> +/**
>>> + * \file lsc_base.h
>>> + * \brief Base types and definitions for LscImplementation class hierarchy
>>> + *
>>> + * Split the common classes and types definitions to a dedicated file to avoid
>>> + * circular inclusions.
>>> + *
>>> + * Both LscGrid and LscPolynomial inherit from LscImplementation and they are
>>> + * instantiated from lsc.h as the LscAlgorithm::init() function is there inlined.
>>> + *
>>> + * For this reason lsc.h needs to include lsc_grid.h and lsc_polynomial.h which
>>> + * need the LscImplementation class definition themselves.
>>> + *
>>> + * Split the LscImplementation interface definition to this file so that it can
>>> + * be included by both lsc_grid.h and lsc_polynomial.h, which are then included
>>> + * by lsc.h.
>>> + */
>>> +
>>> +namespace libcamera {
>>> +
>>> +namespace ipa {
>>> +
>>> +namespace lsc {
>>> +
>>> +/**
>>> + * \class Components
>>> + * \brief Associate a colour components with a list of gains
>>> + * \tparam T The type used to store the gain values when loaded from tuning file
>>> + *
>>> + * Lsc tables are defined as a list of gain values associated to a colour
>>> + * component.
>>> + *
>>> + * As different ISP support different colour components (usually 'r', 'gr',
>>> + * 'gb', 'b' or just 'r', 'g', 'b') this class associates a string
>>> + * identifier for the colour component to a list of gains of type \a T.
>>> + *
>>> + * Each key name shall match an entry in the tuning file and the type \a T
>>> + * shall match the size of the registers where gains are stored.
>>> + *
>>> + * The list of keys is provided to the LscAlgorithm class using \a
>>> + * LscDescriptor::keys.
>>> + */
>>> +
>>> +/**
>>> + * \class ComponentsMap
>>> + * \brief Associate a colour temperature to a lsc table
>>> + * \tparam T The type used to store the gain values when loaded from tuning file
>>> + *
>>> + * An lsc table is generated during the tuning phase for a specific light
>>> + * temperature, and a tuning file usually contains lsc tables generated for
>>> + * several different colour temperatures.
>>> + *
>>> + * This class associates an lsc table to the colour temperature used when the
>>> + * table has been generated.
>>> + */
>>> +
>>> +} /* namespace lsc */
>>> +
>>> +/**
>>> + * \struct LscDescriptor
>>> + * \brief Describe the ISP lsc engine
>>> + *
>>> + * \var LscDescriptor::keys
>>> + * \brief The list of colour components to which a list of gains is associated
>>> + * with in the tuning file. Used for parsing the tuning file
>>> + *
>>> + * \var LscDescriptor::numHCells
>>> + * \brief Number of horizontal cells of the ISP lsc grid. Used for validating
>>> + * the list of gains parsed from tuning file
>>> + *
>>> + * \var LscDescriptor::numVCells
>>> + * \brief Number of vertical cells of the ISP lsc grid. Used for validating
>>> + * the list of gains parsed from tuning file
>>> + *
>>> + * \var LscDescriptor::sensorSize
>>> + * \brief The physical sensor size. This is the largest frame size used to
>>> + * generate the lsc table. Only used by the polynomial lsc algorithm
>>> + */
>>> +
>>> +/**
>>> + * \class LscImplementation
>>> + * \brief Pure virtual base class for lsc algorithm implementations
>>> + * \tparam T The type used to store the gain values when loaded from tuning file
>>> + * \tparam U The fixedpoint format used to convert gain values generated by
>>> + * polynomial expansion to the register format
>>> + *
>>> + * Defines the interface for the lsc algorithm implementation. Currently
>>> + * implemented by LscGrid and LscPolynomial.
>>> + */
>>> +
>>> +/**
>>> + * \fn LscImplementation::~LscImplementation
>>> + * \brief Virtual class destructor
>>> + */
>>> +
>>> +/**
>>> + * \fn LscImplementation::parseLscData
>>> + * \brief Parse \a tuningData using \a descriptor
>>> + * \param[in] tuningData The tuning data
>>> + * \param[in] descriptor The lsc engine descriptor
>>> + *
>>> + * Parse the tuning file using the \a descriptor to identify the colour
>>> + * components in the tuning data and validate the size of the loaded gains
>>> + * tables.
>>> + *
>>> + * \return 0 on success, a negative error number otherwise
>>> + */
>>> +
>>> +/**
>>> + * \fn LscImplementation::resampleLscData
>>> + * \brief Re-sample the lsc components for \a cropRectangle
>>> + * \param[in] cropRectangle The sensor analogue crop rectangle
>>> + * \param[in] xPos List of horizontal positions of the lsc grid nodes
>>> + * \param[in] yPos List of vertical positions of the lsc grid nodes
>>> + *
>>> + * Lsc tables are expressed in two formats:
>>> + * - A list of gain values (LscGrid)
>>> + * - A radial polynomial (LscPolynomial)
>>> + *
>>> + * Grid-based lsc data are generated using an image at a fixed resolution and
>>> + * can't at the moment be re-sampled when a different resolution is used for a
>>> + * streaming session. Re-sampling a grid lsc table will return the same table
>>> + * as loaded from the tuning file.
>>> + *
>>> + * Polynomial are more flexible and can be re-sampled for a given resolution
>>> + * using a list of horizontal and vertical nodes that define the lsc grid.
>>> + * Polynomial lsc tables have to be re-sampled every time a new configuration is
>>> + * applied, as each streaming session might use a different sensor crop
>>> + * rectangle.
>>> + *
>>> + * \a cropRectangle represents the size of the frame on which the polynomial Lsc
>>> + * has to be re-sampled on.
>>> + *
>>> + * \a xPos and \a yPos represent the position of the grid nodes vertexes in
>>> + * the [0, 1] interval. In example an equally spaced grid of 16 nodes will have
>>> + * each segment of size 0.0625 and the list of nodes position will be
>>> + * [0, 0.0625, 0.125, 0.1875, ... , 1]. It is expected that the first position
>>> + * is 0 and the last position is 1.
>>> + */
>>> +
>>> +} /* namespace ipa */
>>> +
>>> +} /* namespace libcamera */
>>> diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
>>> new file mode 100644
>>> index 000000000000..aecae305aa9c
>>> --- /dev/null
>>> +++ b/src/ipa/libipa/lsc_base.h
>>> @@ -0,0 +1,59 @@
>>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>>> +/*
>>> + * Copyright (C) 2026 Ideas on Board Oy
>>> + *
>>> + * Base classes and types for LSC algorithms implementations
>>> + */
>>> +
>>> +#pragma once
>>> +
>>> +#include <map>
>>> +#include <string>
>>> +#include <vector>
>>> +
>>> +#include <libcamera/geometry.h>
>>> +
>>> +#include "libcamera/internal/value_node.h"
>>> +
>>> +namespace libcamera {
>>> +
>>> +namespace ipa {
>>> +
>>> +namespace lsc {
>>> +
>>> +template<typename T>
>>> +class Components : public std::map<std::string, std::vector<T>>
>>> +{
>>> +};
>>> +
>>> +template<typename T>
>>> +class ComponentsMap : public std::map<unsigned int, Components<T>>
>>> +{
>>> +};
>>> +
>>> +} /* namespace lsc */
>>> +
>>> +struct LscDescriptor {
>>> +	std::vector<std::string> keys;
>>> +	unsigned int numHCells;
>>> +	unsigned int numVCells;
>>> +	Size sensorSize;
>>> +};
>>> +
>>> +template<typename T, typename U>
>>
>> I'm not sure we need U here. LscImplementation doesn't use it, and LscGrid
>> doesn't use it...and removing it like so compiles just fine:
>>
>> diff --git a/src/ipa/libipa/lsc.h b/src/ipa/libipa/lsc.h
>> index 8bfa3a7c4..def74cfc8 100644
>> --- a/src/ipa/libipa/lsc.h
>> +++ b/src/ipa/libipa/lsc.h
>> @@ -72,7 +72,7 @@ public:
>>
>>                  std::string type = tuningData["type"].get<std::string>("table");
>>                  if (type == "table") {
>> -                       impl_ = std::make_unique<LscGrid<T, U>>();
>> +                       impl_ = std::make_unique<LscGrid<T>>();
>>                          LOG(Lsc, Debug) << "Using grid-based Lsc";
>>                  } else if (type == "polynomial") {
>>                          impl_ = std::make_unique<LscPolynomial<T, U>>();
>> @@ -134,7 +134,7 @@ public:
>>          }
>>
>>   private:
>> -       std::unique_ptr<LscImplementation<T, U>> impl_;
>> +       std::unique_ptr<LscImplementation<T>> impl_;
>>          Interpolator<lsc::Components<T>> sets_;
>>          lsc::ComponentsMap<T> lscData_;
>>          bool polynomial_;
>> diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
>> index aecae305a..c0c7d47e9 100644
>> --- a/src/ipa/libipa/lsc_base.h
>> +++ b/src/ipa/libipa/lsc_base.h
>> @@ -40,7 +40,7 @@ struct LscDescriptor {
>>          Size sensorSize;
>>   };
>>
>> -template<typename T, typename U>
>> +template<typename T>
>>   class LscImplementation
>>   {
>>   public:
>> diff --git a/src/ipa/libipa/lsc_grid.h b/src/ipa/libipa/lsc_grid.h
>> index e92cde95d..95213841a 100644
>> --- a/src/ipa/libipa/lsc_grid.h
>> +++ b/src/ipa/libipa/lsc_grid.h
>> @@ -19,8 +19,8 @@ LOG_DECLARE_CATEGORY(LscGrid)
>>
>>   namespace ipa {
>>
>> -template<typename T, typename U>
>> -class LscGrid : public LscImplementation<T, U>
>> +template<typename T>
>> +class LscGrid : public LscImplementation<T>
>>   {
>>   public:
>>          ~LscGrid() {}
>> diff --git a/src/ipa/libipa/lsc_polynomial.h b/src/ipa/libipa/lsc_polynomial.h
>> index 72cca591f..482c5fce8 100644
>> --- a/src/ipa/libipa/lsc_polynomial.h
>> +++ b/src/ipa/libipa/lsc_polynomial.h
>> @@ -69,7 +69,7 @@ protected:
>>   };
>>
>>   template<typename T, typename U>
>> -class LscPolynomial : public LscPolynomialBase, public LscImplementation<T, U>
>> +class LscPolynomial : public LscPolynomialBase, public LscImplementation<T>
>>   {
>>   public:
>>          ~LscPolynomial() {}
>>
>>
>>
>> So I was going to say "Can we just remove it", buuuuuuut it occurs to me
>> that it will be easier to support different hardware with a single
>> implementation of the tuning scripts, if those tuning scripts can just
>> return a _gain_ value rather than the quantised gain and then have the
>> algorithm return the quantised array to the IPA's implementation...so rather
>> than:
>>
>>    - Lsc:
>>        sets:
>>          - ct: 2500
>>            r: [
>>                3461, 3403, 3318, ...
>>            ]
>>            g: [
>>                ...,
>>            ]
>>            ...
>>
>> We have
>>
>>    - Lsc:
>>        sets:
>>          - ct: 2500
>>            r: [
>>                3.380, 3.323, 3.240, ...
>>            ]
>>            g: [
>>                ...,
>>            ]
>>            ...
>>
>> And then we send to the IPA implementation the output of U(r[n]).quantized()...what do you think?
>>
> 
> I think that's the direction where we want to go
> 
> However it is not clear to me how you would remove U even with this
> version, as LscPolynomial uses it in samplePolynomial()
> 
> 		for (double y : yPositions) {
> 			for (double x : xPositions) {
> 				double xp = x0 + x * w;
> 				double yp = y0 + y * h;
> 				float sample = static_cast<float>
> 						(poly.sampleAtNormalizedPixelPos(xp, yp));
> 
> 				samples.push_back(U(sample).quantized());
> 			}
> 		}
> 
> What have I missed ?

U isn't removed from LscPolynomial; that's still LscPolynomial<T, U>. It's only dropped from LscGrid 
and LscImplementation...but if we want to move to having the tuning files specify raw gains then 
we'll need that in Grid too, so we can just leave this as-is.
> 
> 
>>> +class LscImplementation
>>> +{
>>> +public:
>>> +	virtual ~LscImplementation() = default;
>>> +
>>> +	virtual int parseLscData(const ValueNode &tuningData,
>>> +				 const LscDescriptor &descriptor) = 0;
>>> +
>>> +	virtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
>>> +						      const std::vector<double> &xPos,
>>> +						      const std::vector<double> &yPos) = 0;
>>> +};
>>> +
>>> +} /* namespace ipa */
>>> +
>>> +} /* namespace libcamera */
>>> diff --git a/src/ipa/libipa/lsc_grid.cpp b/src/ipa/libipa/lsc_grid.cpp
>>> new file mode 100644
>>> index 000000000000..878994acc8c9
>>> --- /dev/null
>>> +++ b/src/ipa/libipa/lsc_grid.cpp
>>> @@ -0,0 +1,33 @@
>>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>>> +/*
>>> + * Copyright (C) 2026, Ideas On Board
>>> + *
>>> + * Grid based lens shading correction
>>> + */
>>> +
>>> +#include "lsc_grid.h"
>>> +
>>> +/**
>>> + * \file lsc_grid.h
>>> + * \brief LscGrid class
>>> + */
>>> +
>>> +namespace libcamera {
>>> +
>>> +LOG_DEFINE_CATEGORY(LscGrid)
>>> +
>>> +namespace ipa {
>>> +
>>> +/**
>>> + * \class LscGrid
>>> + * \brief Grid based lsc algorithm implementation
>>> + *
>>> + * Grid based lsc algorithm implementation. The LscGrid class implements lsc
>>> + * support using tabular lsc data.
>>> + *
>>> + * \sa LscImplementation
>>> + * \sa LscAlgorithm
>>> + */
>>> +
>>> +} /* namespace ipa */
>>> +} /* namespace libcamera */
>>> diff --git a/src/ipa/libipa/lsc_grid.h b/src/ipa/libipa/lsc_grid.h
>>> new file mode 100644
>>> index 000000000000..e92cde95d18d
>>> --- /dev/null
>>> +++ b/src/ipa/libipa/lsc_grid.h
>>> @@ -0,0 +1,113 @@
>>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>>> +/*
>>> + * Copyright (C) 2026, Ideas On Board
>>> + *
>>> + * Grid based lens shading correction
>>> + */
>>> +
>>> +#pragma once
>>> +
>>> +#include <vector>
>>> +
>>> +#include <libcamera/base/log.h>
>>> +
>>> +#include "lsc_base.h"
>>> +
>>> +namespace libcamera {
>>> +
>>> +LOG_DECLARE_CATEGORY(LscGrid)
>>> +
>>> +namespace ipa {
>>> +
>>> +template<typename T, typename U>
>>> +class LscGrid : public LscImplementation<T, U>
>>> +{
>>> +public:
>>> +	~LscGrid() {}
>>> +
>>> +	int parseLscData(const ValueNode &yamlSets,
>>> +			 const LscDescriptor &descriptor) override
>>> +	{
>>> +		const auto &sets = yamlSets.asList();
>>> +
>>> +		for (const auto &yamlSet : sets) {
>>> +			uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
>>> +
>>> +			int ret = parseLscComponent(yamlSet, ct, descriptor);
>>> +			if (ret)
>>> +				return ret;
>>> +		}
>>> +
>>> +		if (lscData_.empty()) {
>>> +			LOG(LscGrid, Error) << "Failed to load any sets";
>>> +			return -EINVAL;
>>> +		}
>>> +
>>> +		return 0;
>>> +	}
>>> +
>>> +	lsc::ComponentsMap<T> resampleLscData([[maybe_unused]] const Rectangle &cropRectangle,
>>> +					      [[maybe_unused]] const std::vector<double> &xPos,
>>> +					      [[maybe_unused]] const std::vector<double> &yPos) override
>>> +	{
>>> +		/* No resampling for grid-based LSC algorithm. */
>>> +		return lscData_;
>>> +	}
>>> +
>>> +private:
>>> +	int parseLscComponent(const ValueNode &yamlSet,
>>> +			      unsigned int ct, const LscDescriptor &descriptor)
>>> +	{
>>> +		lsc::Components<T> component;
>>> +		for (auto &k : descriptor.keys) {
>>> +			auto [it, inserted] = component.emplace(
>>> +				std::piecewise_construct,
>>> +				std::forward_as_tuple(k.c_str()),
>>> +				std::forward_as_tuple(parseTable(yamlSet,
>>> +								 k.c_str(),
>>> +								 descriptor.numHCells,
>>> +								 descriptor.numVCells)));
>>> +			if (!inserted || it->second.empty()) {
>>> +				LOG(LscGrid, Error)
>>> +					<< "Set " << k << " for color temperature "
>>> +					<< ct << " is missing";
>>> +				return -EINVAL;
>>> +			}
>>> +		}
>>> +
>>> +		auto [it, inserted] = lscData_.emplace(ct, component);
>>> +		if (!inserted) {
>>> +			LOG(LscGrid, Error)
>>> +				<< "Multiple sets found for color temperature "
>>> +				<< ct;
>>> +			return -EINVAL;
>>> +		}
>>> +
>>> +		return 0;
>>> +	}
>>> +
>>> +	std::vector<T> parseTable(const ValueNode &tuningData,
>>> +				  const char *prop, unsigned int numHCells,
>>> +				  unsigned int numVCells)
>>> +	{
>>> +		unsigned int kLscNumSamples = numHCells * numVCells;
>>> +
>>> +		std::vector<T> table =
>>> +			tuningData[prop].get<std::vector<T>>().value_or(utils::defopt);
>>> +		if (table.size() != kLscNumSamples) {
>>> +			LOG(LscGrid, Error)
>>> +				<< "Invalid '" << prop << "' values: expected "
>>> +				<< kLscNumSamples
>>> +				<< " elements, got " << table.size();
>>> +			return {};
>>> +		}
>>> +
>>> +		return table;
>>> +	}
>>> +
>>> +	lsc::ComponentsMap<T> lscData_;
>>> +};
>>> +
>>> +} /* namespace ipa */
>>> +
>>> +} /* namespace libcamera */
>>> diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
>>> index f607d86c54c3..8d3f1883d6a5 100644
>>> --- a/src/ipa/libipa/lsc_polynomial.cpp
>>> +++ b/src/ipa/libipa/lsc_polynomial.cpp
>>> @@ -1,12 +1,14 @@
>>>    /* SPDX-License-Identifier: LGPL-2.1-or-later */
>>>    /*
>>> - * Copyright (C) 2024, Ideas On Board
>>> + * Copyright (C) 2026, Ideas On Board
>>>     *
>>> - * Polynomial class to represent lens shading correction
>>> + * Polynomial based lens shading correction
>>>     */
>>>    #include "lsc_polynomial.h"
>>> +#include <assert.h>
>>> +
>>>    #include <libcamera/base/log.h>
>>>    /**
>>> @@ -21,7 +23,7 @@ LOG_DEFINE_CATEGORY(LscPolynomial)
>>>    namespace ipa {
>>>    /**
>>> - * \class LscPolynomial
>>> + * \class Polynomial
>>>     * \brief Class for handling even polynomials used in lens shading correction
>>>     *
>>>     * Shading artifacts of camera lenses can be modeled using even radial
>>> @@ -31,7 +33,7 @@ namespace ipa {
>>>     */
>>>    /**
>>> - * \fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
>>> + * \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
>>>    		      double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
>>>    		      double k4 = 0.0)
>>>     * \brief Construct a polynomial using the given coefficients
>>> @@ -45,7 +47,13 @@ namespace ipa {
>>>     */
>>>    /**
>>> - * \fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)
>>> + * \fn Polynomial::Polynomial(const Polynomial &other)
>>> + * \brief Construct a Polynomial by copy
>>> + * \param[in] other The Polynomial to copy construct from
>>> + */
>>> +
>>> +/**
>>> + * \fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)
>>>     * \brief Sample the polynomial at the given normalized pixel position
>>>     *
>>>     * This functions samples the polynomial at the given pixel position divided by
>>> @@ -55,9 +63,21 @@ namespace ipa {
>>>     * \param y y position in normalized coordinates
>>>     * \return The sampled value
>>>     */
>>> +double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
>>> +{
>>> +	double dx = x - cnx_;
>>> +	double dy = y - cny_;
>>> +	double r = sqrt(dx * dx + dy * dy);
>>> +	double res = 1.0;
>>> +
>>> +	for (unsigned int i = 0; i < coefficients_.size(); i++)
>>> +		res += coefficients_[i] * std::pow(r, (i + 1) * 2);
>>> +
>>> +	return res;
>>> +}
>>>    /**
>>> - * \fn LscPolynomial::getM()
>>> + * \fn Polynomial::getM()
>>>     * \brief Get the value m as described in the dng specification
>>>     *
>>>     * Returns m according to dng spec. m represents the Euclidean distance
>>> @@ -66,9 +86,18 @@ namespace ipa {
>>>     *
>>>     * \return The sampled value
>>>     */
>>> +double Polynomial::getM() const
>>> +{
>>> +	double cpx = imageSize_.width * cx_;
>>> +	double cpy = imageSize_.height * cy_;
>>> +	double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
>>> +	double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
>>> +
>>> +	return sqrt(mx * mx + my * my);
>>> +}
>>>    /**
>>> - * \fn LscPolynomial::setReferenceImageSize(const Size &size)
>>> + * \fn Polynomial::setReferenceImageSize(const Size &size)
>>>     * \brief Set the reference image size
>>>     *
>>>     * Set the reference image size that is used for subsequent calls to getM() and
>>> @@ -76,6 +105,92 @@ namespace ipa {
>>>     *
>>>     * \param size The size of the reference image
>>>     */
>>> +void Polynomial::setReferenceImageSize(const Size &size)
>>> +{
>>> +	assert(!size.isNull());
>>> +	imageSize_ = size;
>>> +
>>> +	/* Calculate normalized centers */
>>> +	double m = getM();
>>> +	cnx_ = (size.width * cx_) / m;
>>> +	cny_ = (size.height * cy_) / m;
>>> +}
>>> +
>>> +/**
>>> + * \class LscPolynomialBase
>>> + * \brief Base class for LscPolynomial
>>> + *
>>> + * Base class for LscPolynomial for non-templated functions.
>>> + */
>>> +
>>> +/**
>>> + * \brief Parse polynomial lsc data
>>> + * \param[in] yamlSets The tuning file content
>>> + * \param[in] descriptor The lsc engine descriptor
>>> + *
>>> + * Parse the lsc data in polyomial form from the \a yamlSet tuning data.
>>> + */
>>> +int LscPolynomialBase::parseLscData(const ValueNode &yamlSets,
>>> +				    const LscDescriptor &descriptor)
>>> +{
>>> +	const auto &sets = yamlSets.asList();
>>> +	for (const auto &yamlSet : sets) {
>>> +		uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
>>> +
>>> +		Components components;
>>> +		for (auto &k : descriptor.keys) {
>>> +			auto polynomial = yamlSet[k.c_str()].get<Polynomial>();
>>> +			if (!polynomial) {
>>> +				LOG(LscPolynomial, Error)
>>> +					<< "Missing polynomial for component "
>>> +					<< k;
>>> +				return -EINVAL;
>>> +			}
>>> +
>>> +			auto [it, inserted] =
>>> +				components.emplace(std::piecewise_construct,
>>> +						   std::forward_as_tuple(k.c_str()),
>>> +						   std::forward_as_tuple(*polynomial));
>>> +
>>> +			it->second.setReferenceImageSize(descriptor.sensorSize);
>>> +		}
>>> +
>>> +		auto [it, inserted] = lscData_.emplace(ct, components);
>>> +		if (!inserted) {
>>> +			LOG(LscPolynomial, Error)
>>> +				<< "Multiple sets found for "
>>> +				<< "color temperature " << ct;
>>> +			return -EINVAL;
>>> +		}
>>> +	}
>>> +
>>> +	if (lscData_.empty()) {
>>> +		LOG(LscPolynomial, Error) << "Failed to load any sets";
>>> +		return -EINVAL;
>>> +	}
>>> +
>>> +	return 0;
>>> +}
>>> +
>>> +/**
>>> + * \var LscPolynomialBase::lscData_
>>> + * \brief The polynomial lsc data
>>> + *
>>> + * Maps colour temperatures to per-colour radial polynomial definitions.
>>> + */
>>> +
>>> +/**
>>> + * \class LscPolynomial
>>> + * \brief Radial Polynomial lsc algorithm implementation
>>> + *
>>> + * Polynomial-based lsc algorithm implementation. The LscPolynomial class
>>> + * implements lsc support using Polynomial to represent the shading artifacts
>>> + * map.
>>> + *
>>> + * \sa LscImplementation
>>> + * \sa LscAlgorithm
>>> + *
>>> + */
>>> -} // namespace ipa
>>> -} // namespace libcamera
>>> +} /* namespace ipa */
>>> +} /* namespace libcamera */
>>> diff --git a/src/ipa/libipa/lsc_polynomial.h b/src/ipa/libipa/lsc_polynomial.h
>>> index d7d9ae42e360..72cca591f260 100644
>>> --- a/src/ipa/libipa/lsc_polynomial.h
>>> +++ b/src/ipa/libipa/lsc_polynomial.h
>>> @@ -1,15 +1,17 @@
>>>    /* SPDX-License-Identifier: LGPL-2.1-or-later */
>>>    /*
>>> - * Copyright (C) 2024, Ideas On Board
>>> + * Copyright (C) 2026, Ideas On Board
>>>     *
>>> - * Helper for radial polynomial used in lens shading correction.
>>> + * Polynomial based lens shading correction
>>>     */
>>> +
>>>    #pragma once
>>>    #include <algorithm>
>>>    #include <array>
>>> -#include <assert.h>
>>>    #include <cmath>
>>> +#include <optional>
>>> +#include <tuple>
>>>    #include <libcamera/base/log.h>
>>>    #include <libcamera/base/span.h>
>>> @@ -18,64 +20,117 @@
>>>    #include "libcamera/internal/value_node.h"
>>> +#include "lsc_base.h"
>>> +
>>>    namespace libcamera {
>>>    LOG_DECLARE_CATEGORY(LscPolynomial)
>>>    namespace ipa {
>>> -class LscPolynomial
>>> +class Polynomial
>>>    {
>>>    public:
>>> -	LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
>>> -		      double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
>>> -		      double k4 = 0.0)
>>> +	Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
>>> +		   double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
>>> +		   double k4 = 0.0)
>>>    		: cx_(cx), cy_(cy), cnx_(0), cny_(0),
>>>    		  coefficients_({ k0, k1, k2, k3, k4 })
>>>    	{
>>>    	}
>>> -	double sampleAtNormalizedPixelPos(double x, double y) const
>>> +	Polynomial(const Polynomial &other) = default;
>>> +
>>> +	double sampleAtNormalizedPixelPos(double x, double y) const;
>>> +	double getM() const;
>>> +	void setReferenceImageSize(const Size &size);
>>> +
>>> +private:
>>> +	double cx_;
>>> +	double cy_;
>>> +	double cnx_;
>>> +	double cny_;
>>> +	std::array<double, 5> coefficients_;
>>> +	Size imageSize_;
>>> +};
>>> +
>>> +class LscPolynomialBase
>>> +{
>>> +private:
>>> +	using Components = std::map<std::string, Polynomial>;
>>> +	using ComponentsMap = std::map<unsigned int, Components>;
>>> +
>>> +public:
>>> +	int parseLscData(const ValueNode &yamlSets,
>>> +			 const LscDescriptor &descriptor);
>>> +
>>> +protected:
>>> +	ComponentsMap lscData_;
>>> +};
>>> +
>>> +template<typename T, typename U>
>>> +class LscPolynomial : public LscPolynomialBase, public LscImplementation<T, U>
>>> +{
>>> +public:
>>> +	~LscPolynomial() {}
>>> +
>>> +	int parseLscData(const ValueNode &yamlSets,
>>> +			 const LscDescriptor &descriptor) override
>>>    	{
>>> -		double dx = x - cnx_;
>>> -		double dy = y - cny_;
>>> -		double r = sqrt(dx * dx + dy * dy);
>>> -		double res = 1.0;
>>> -		for (unsigned int i = 0; i < coefficients_.size(); i++) {
>>> -			res += coefficients_[i] * std::pow(r, (i + 1) * 2);
>>> -		}
>>> -		return res;
>>> +		return LscPolynomialBase::parseLscData(yamlSets, descriptor);
>>>    	}
>>> -	double getM() const
>>> +	lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
>>> +					      const std::vector<double> &xPos,
>>> +					      const std::vector<double> &yPos) override
>>>    	{
>>> -		double cpx = imageSize_.width * cx_;
>>> -		double cpy = imageSize_.height * cy_;
>>> -		double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
>>> -		double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
>>> +		lsc::ComponentsMap<T> components;
>>> -		return sqrt(mx * mx + my * my);
>>> -	}
>>> +		for (auto &[t, c] : lscData_) {
>>> +			lsc::Components<T> comp;
>>> -	void setReferenceImageSize(const Size &size)
>>> -	{
>>> -		assert(!size.isNull());
>>> -		imageSize_ = size;
>>> +			for (auto &[k, p] : c) {
>>> +				comp.emplace(std::piecewise_construct,
>>> +					     std::forward_as_tuple(k),
>>> +					     std::forward_as_tuple(samplePolynomial(p, xPos, yPos,
>>> +										    cropRectangle)));
>>> +			}
>>> -		/* Calculate normalized centers */
>>> -		double m = getM();
>>> -		cnx_ = (size.width * cx_) / m;
>>> -		cny_ = (size.height * cy_) / m;
>>> +			components[t] = comp;
>>> +		}
>>> +
>>> +		return components;
>>>    	}
>>>    private:
>>> -	double cx_;
>>> -	double cy_;
>>> -	double cnx_;
>>> -	double cny_;
>>> -	std::array<double, 5> coefficients_;
>>> +	std::vector<T> samplePolynomial(const Polynomial &poly,
>>> +					Span<const double> xPositions,
>>> +					Span<const double> yPositions,
>>> +					const Rectangle &cropRectangle)
>>> -	Size imageSize_;
>>> +	{
>>> +		double m = poly.getM();
>>> +		double x0 = cropRectangle.x / m;
>>> +		double y0 = cropRectangle.y / m;
>>> +		double w = cropRectangle.width / m;
>>> +		double h = cropRectangle.height / m;
>>> +		std::vector<T> samples;
>>> +
>>> +		samples.reserve(xPositions.size() * yPositions.size());
>>> +
>>> +		for (double y : yPositions) {
>>> +			for (double x : xPositions) {
>>> +				double xp = x0 + x * w;
>>> +				double yp = y0 + y * h;
>>> +				float sample = static_cast<float>
>>> +						(poly.sampleAtNormalizedPixelPos(xp, yp));
>>> +
>>> +				samples.push_back(U(sample).quantized());
>>> +			}
>>> +		}
>>> +
>>> +		return samples;
>>> +	}
>>>    };
>>>    } /* namespace ipa */
>>> @@ -83,8 +138,8 @@ private:
>>>    #ifndef __DOXYGEN__
>>>    template<>
>>> -struct ValueNode::Accessor<ipa::LscPolynomial> {
>>> -	std::optional<ipa::LscPolynomial> get(const ValueNode &obj) const
>>> +struct ValueNode::Accessor<ipa::Polynomial> {
>>> +	std::optional<ipa::Polynomial> get(const ValueNode &obj) const
>>>    	{
>>>    		std::optional<double> cx = obj["cx"].get<double>();
>>>    		std::optional<double> cy = obj["cy"].get<double>();
>>> @@ -98,7 +153,7 @@ struct ValueNode::Accessor<ipa::LscPolynomial> {
>>>    			LOG(LscPolynomial, Error)
>>>    				<< "Polynomial is missing a parameter";
>>> -		return ipa::LscPolynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);
>>> +		return ipa::Polynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);
>>>    	}
>>>    };
>>> diff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build
>>> index edf8eabd8b78..da6ea0c5e130 100644
>>> --- a/src/ipa/libipa/meson.build
>>> +++ b/src/ipa/libipa/meson.build
>>> @@ -14,6 +14,9 @@ libipa_headers = files([
>>>        'fixedpoint.h',
>>>        'histogram.h',
>>>        'interpolator.h',
>>> +    'lsc.h',
>>> +    'lsc_base.h',
>>> +    'lsc_grid.h',
>>>        'lsc_polynomial.h',
>>>        'lux.h',
>>>        'module.h',
>>> @@ -36,6 +39,8 @@ libipa_sources = files([
>>>        'fixedpoint.cpp',
>>>        'histogram.cpp',
>>>        'interpolator.cpp',
>>> +    'lsc.cpp',
>>> +    'lsc_grid.cpp',
>>>        'lsc_polynomial.cpp',
>>>        'lux.cpp',
>>>        'module.cpp',
>>> diff --git a/src/ipa/rkisp1/algorithms/lsc.cpp b/src/ipa/rkisp1/algorithms/lsc.cpp
>>> index 25f7c512b35c..51076737ffa5 100644
>>> --- a/src/ipa/rkisp1/algorithms/lsc.cpp
>>> +++ b/src/ipa/rkisp1/algorithms/lsc.cpp
>>> @@ -2,7 +2,7 @@
>>>    /*
>>>     * Copyright (C) 2021-2022, Ideas On Board
>>>     *
>>> - * RkISP1 Lens Shading Correction control
>>> + * RkISP1 Lens Shading Correction algorithm
>>>     */
>>>    #include "lsc.h"
>>> @@ -14,56 +14,17 @@
>>>    #include <libcamera/base/log.h>
>>>    #include <libcamera/base/utils.h>
>>> -#include "libcamera/internal/value_node.h"
>>> -
>>> -#include "libipa/lsc_polynomial.h"
>>> -#include "linux/rkisp1-config.h"
>>> -
>>>    /**
>>>     * \file lsc.h
>>>     */
>>>    namespace libcamera {
>>> -namespace ipa {
>>> -
>>> -template<typename T>
>>> -void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,
>>> -		       std::vector<T> &dest, double lambda)
>>> -{
>>> -	ASSERT(a.size() == b.size());
>>> -	dest.resize(a.size());
>>> -	for (size_t i = 0; i < a.size(); i++)
>>> -		dest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;
>>> -}
>>> -
>>> -template<>
>>> -void Interpolator<rkisp1::algorithms::LensShadingCorrection::Components>::
>>> -	interpolate(const rkisp1::algorithms::LensShadingCorrection::Components &a,
>>> -		    const rkisp1::algorithms::LensShadingCorrection::Components &b,
>>> -		    rkisp1::algorithms::LensShadingCorrection::Components &dest,
>>> -		    double lambda)
>>> -{
>>> -	interpolateVector(a.r, b.r, dest.r, lambda);
>>> -	interpolateVector(a.gr, b.gr, dest.gr, lambda);
>>> -	interpolateVector(a.gb, b.gb, dest.gb, lambda);
>>> -	interpolateVector(a.b, b.b, dest.b, lambda);
>>> -}
>>> -
>>> -} /* namespace ipa */
>>> -
>>>    namespace ipa::rkisp1::algorithms {
>>>    /**
>>>     * \class LensShadingCorrection
>>>     * \brief RkISP1 Lens Shading Correction control
>>> - *
>>> - * Due to the optical characteristics of the lens, the light intensity received
>>> - * by the sensor is not uniform.
>>> - *
>>> - * The Lens Shading Correction algorithm applies multipliers to all pixels
>>> - * to compensate for the lens shading effect. The coefficients are
>>> - * specified in a downscaled table in the YAML tuning file.
>>>     */
>>>    LOG_DEFINE_CATEGORY(RkISP1Lsc)
>>> @@ -72,265 +33,20 @@ namespace {
>>>    constexpr int kColourTemperatureQuantization = 10;
>>> -class LscPolynomialShadingDescriptor : public LensShadingCorrection::ShadingDescriptor
>>> -{
>>> -public:
>>> -	LscPolynomialShadingDescriptor(const LscPolynomial &pr, const LscPolynomial &pgr,
>>> -				       const LscPolynomial &pgb, const LscPolynomial &pb)
>>> -		: pr_(pr), pgr_(pgr), pgb_(pgb), pb_(pb)
>>> -	{
>>> -	}
>>> -
>>> -	LensShadingCorrection::Components sampleForCrop(const Rectangle &cropRectangle,
>>> -							Span<const double> xSizes,
>>> -							Span<const double> ySizes) override;
>>> -
>>> -private:
>>> -	std::vector<uint16_t> samplePolynomial(const LscPolynomial &poly,
>>> -					       Span<const double> xPositions,
>>> -					       Span<const double> yPositions,
>>> -					       const Rectangle &cropRectangle);
>>> -
>>> -	std::vector<double> sizesListToPositions(Span<const double> sizes);
>>> -
>>> -	LscPolynomial pr_;
>>> -	LscPolynomial pgr_;
>>> -	LscPolynomial pgb_;
>>> -	LscPolynomial pb_;
>>> -};
>>> -
>>> -LensShadingCorrection::Components
>>> -LscPolynomialShadingDescriptor::sampleForCrop(const Rectangle &cropRectangle,
>>> -					      Span<const double> xSizes,
>>> -					      Span<const double> ySizes)
>>> -{
>>> -	std::vector<double> xPos = sizesListToPositions(xSizes);
>>> -	std::vector<double> yPos = sizesListToPositions(ySizes);
>>> -
>>> -	return {
>>> -		.r = samplePolynomial(pr_, xPos, yPos, cropRectangle),
>>> -		.gr = samplePolynomial(pgr_, xPos, yPos, cropRectangle),
>>> -		.gb = samplePolynomial(pgb_, xPos, yPos, cropRectangle),
>>> -		.b = samplePolynomial(pb_, xPos, yPos, cropRectangle)
>>> -	};
>>> -}
>>> -
>>> -std::vector<uint16_t>
>>> -LscPolynomialShadingDescriptor::samplePolynomial(const LscPolynomial &poly,
>>> -						 Span<const double> xPositions,
>>> -						 Span<const double> yPositions,
>>> -						 const Rectangle &cropRectangle)
>>> -{
>>> -	double m = poly.getM();
>>> -	double x0 = cropRectangle.x / m;
>>> -	double y0 = cropRectangle.y / m;
>>> -	double w = cropRectangle.width / m;
>>> -	double h = cropRectangle.height / m;
>>> -	std::vector<uint16_t> samples;
>>> -
>>> -	samples.reserve(xPositions.size() * yPositions.size());
>>> -
>>> -	for (double y : yPositions) {
>>> -		for (double x : xPositions) {
>>> -			double xp = x0 + x * w;
>>> -			double yp = y0 + y * h;
>>> -			/*
>>> -			 * The hardware uses 2.10 fixed point format and limits
>>> -			 * the legal values to [1..3.999]. Scale and clamp the
>>> -			 * sampled value accordingly.
>>> -			 */
>>> -			int v = static_cast<int>(
>>> -				poly.sampleAtNormalizedPixelPos(xp, yp) *
>>> -				1024);
>>> -			v = std::clamp(v, 1024, 4095);
>>> -			samples.push_back(v);
>>> -		}
>>> -	}
>>> -	return samples;
>>> -}
>>> -
>>> -/*
>>> - * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left
>>> - * quadrant of the grid. This is then mirrored in hardware to the other
>>> - * quadrants. See parseSizes() for further details. For easier handling, this
>>> - * function converts the cell sizes of half the grid to a list of position of
>>> - * the whole grid (on one axis). Example:
>>> - *
>>> - * input:   | 0.2 | 0.3 |
>>> - * output: 0.0   0.2   0.5   0.8   1.0
>>> - */
>>> -std::vector<double>
>>> -LscPolynomialShadingDescriptor::sizesListToPositions(Span<const double> sizes)
>>> -{
>>> -	const int half = sizes.size();
>>> -	std::vector<double> positions(half * 2 + 1);
>>> -	double x = 0.0;
>>> -
>>> -	positions[half] = 0.5;
>>> -	for (int i = 1; i <= half; i++) {
>>> -		x += sizes[half - i];
>>> -		positions[half - i] = 0.5 - x;
>>> -		positions[half + i] = 0.5 + x;
>>> -	}
>>> -
>>> -	return positions;
>>> -}
>>> -
>>> -class LscPolynomialLoader
>>> -{
>>> -public:
>>> -	LscPolynomialLoader(const Size &sensorSize)
>>> -		: sensorSize_(sensorSize)
>>> -	{
>>> -	}
>>> -
>>> -	int parseLscData(const ValueNode &yamlSets,
>>> -			 LensShadingCorrection::ShadingDescriptorMap &lscData);
>>> -
>>> -private:
>>> -	Size sensorSize_;
>>> -};
>>> -
>>> -int LscPolynomialLoader::parseLscData(const ValueNode &yamlSets,
>>> -				      LensShadingCorrection::ShadingDescriptorMap &lscData)
>>> +unsigned int quantize(unsigned int value, unsigned int step)
>>
>> I ended up using this in the IPU3 implementation too; perhaps it can move
>> into the LscAlgorithm somewhere...though that can happen on-top of course.
>>
>> That's my last comment. It's really nice; implementing the IPU3 lens shading
>> algorithm was a real breeze using this, so I think it works very very well
>> at the goal of reducing the amount of duplicated code between the IPAs. I
>> think we should settle the question above of whether to have "raw" gain or
>> quantized gains (which are IPA module specific) in the tuning files and then
>> this is good to go.
>>
> 
> I wouldn't do that right now, but rather on top, once all
> implementations have moved to use LscAlgorithm so we can change the
> tuning file format in one go ?

Sure, works for me, in which case:

Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>
> 
>>
>>>    {
>>> -	const auto &sets = yamlSets.asList();
>>> -	for (const auto &yamlSet : sets) {
>>> -		std::optional<LscPolynomial> pr, pgr, pgb, pb;
>>> -		uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
>>> -
>>> -		if (lscData.count(ct)) {
>>> -			LOG(RkISP1Lsc, Error)
>>> -				<< "Multiple sets found for "
>>> -				<< "color temperature " << ct;
>>> -			return -EINVAL;
>>> -		}
>>> -
>>> -		pr = yamlSet["r"].get<LscPolynomial>();
>>> -		pgr = yamlSet["gr"].get<LscPolynomial>();
>>> -		pgb = yamlSet["gb"].get<LscPolynomial>();
>>> -		pb = yamlSet["b"].get<LscPolynomial>();
>>> -
>>> -		if (!(pr || pgr || pgb || pb)) {
>>> -			LOG(RkISP1Lsc, Error)
>>> -				<< "Failed to parse polynomial for "
>>> -				<< "colour temperature " << ct;
>>> -			return -EINVAL;
>>> -		}
>>> -
>>> -		pr->setReferenceImageSize(sensorSize_);
>>> -		pgr->setReferenceImageSize(sensorSize_);
>>> -		pgb->setReferenceImageSize(sensorSize_);
>>> -		pb->setReferenceImageSize(sensorSize_);
>>> -
>>> -		lscData.emplace(
>>> -			ct, std::make_unique<LscPolynomialShadingDescriptor>(
>>> -				    *pr, *pgr, *pgb, *pb));
>>> -	}
>>> -
>>> -	if (lscData.empty()) {
>>> -		LOG(RkISP1Lsc, Error) << "Failed to load any sets";
>>> -		return -EINVAL;
>>> -	}
>>> -
>>> -	return 0;
>>> +	return std::lround(value / static_cast<double>(step)) * step;
>>>    }
>>> -class LscTableShadingDescriptor : public LensShadingCorrection::ShadingDescriptor
>>> -{
>>> -public:
>>> -	LscTableShadingDescriptor(LensShadingCorrection::Components components)
>>> -		: lscData_(std::move(components))
>>> -	{
>>> -	}
>>> -
>>> -	LensShadingCorrection::Components
>>> -	sampleForCrop([[maybe_unused]] const Rectangle &cropRectangle,
>>> -		      [[maybe_unused]] Span<const double> xSizes,
>>> -		      [[maybe_unused]] Span<const double> ySizes) override
>>> -	{
>>> -		LOG(RkISP1Lsc, Warning)
>>> -			<< "Tabular LSC data doesn't support resampling";
>>> -		return lscData_;
>>> -	}
>>> -
>>> -private:
>>> -	LensShadingCorrection::Components lscData_;
>>> -};
>>> -
>>> -class LscTableLoader
>>> -{
>>> -public:
>>> -	int parseLscData(const ValueNode &yamlSets,
>>> -			 LensShadingCorrection::ShadingDescriptorMap &lscData);
>>> -
>>> -private:
>>> -	std::vector<uint16_t> parseTable(const ValueNode &tuningData,
>>> -					 const char *prop);
>>> -};
>>> -
>>> -int LscTableLoader::parseLscData(const ValueNode &yamlSets,
>>> -				 LensShadingCorrection::ShadingDescriptorMap &lscData)
>>> -{
>>> -	const auto &sets = yamlSets.asList();
>>> -
>>> -	for (const auto &yamlSet : sets) {
>>> -		uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
>>> -
>>> -		if (lscData.count(ct)) {
>>> -			LOG(RkISP1Lsc, Error)
>>> -				<< "Multiple sets found for color temperature "
>>> -				<< ct;
>>> -			return -EINVAL;
>>> -		}
>>> -
>>> -		LensShadingCorrection::Components set;
>>> -		set.r = parseTable(yamlSet, "r");
>>> -		set.gr = parseTable(yamlSet, "gr");
>>> -		set.gb = parseTable(yamlSet, "gb");
>>> -		set.b = parseTable(yamlSet, "b");
>>> -
>>> -		if (set.r.empty() || set.gr.empty() ||
>>> -		    set.gb.empty() || set.b.empty()) {
>>> -			LOG(RkISP1Lsc, Error)
>>> -				<< "Set for color temperature " << ct
>>> -				<< " is missing tables";
>>> -			return -EINVAL;
>>> -		}
>>> -
>>> -		lscData.emplace(
>>> -			ct, std::make_unique<LscTableShadingDescriptor>(std::move(set)));
>>> -	}
>>> -
>>> -	if (lscData.empty()) {
>>> -		LOG(RkISP1Lsc, Error) << "Failed to load any sets";
>>> -		return -EINVAL;
>>> -	}
>>> -
>>> -	return 0;
>>> -}
>>> +} /* namespace */
>>> -std::vector<uint16_t> LscTableLoader::parseTable(const ValueNode &tuningData,
>>> -						 const char *prop)
>>> +LensShadingCorrection::LensShadingCorrection()
>>> +	: lastAppliedCt_(0), lastAppliedQuantizedCt_(0)
>>>    {
>>> -	static constexpr unsigned int kLscNumSamples =
>>> -		RKISP1_CIF_ISP_LSC_SAMPLES_MAX * RKISP1_CIF_ISP_LSC_SAMPLES_MAX;
>>> -
>>> -	std::vector<uint16_t> table =
>>> -		tuningData[prop].get<std::vector<uint16_t>>().value_or(utils::defopt);
>>> -	if (table.size() != kLscNumSamples) {
>>> -		LOG(RkISP1Lsc, Error)
>>> -			<< "Invalid '" << prop << "' values: expected "
>>> -			<< kLscNumSamples
>>> -			<< " elements, got " << table.size();
>>> -		return {};
>>> -	}
>>> -
>>> -	return table;
>>>    }
>>> -std::vector<double> parseSizes(const ValueNode &tuningData,
>>> -			       const char *prop)
>>> +std::vector<double> LensShadingCorrection::parseSizes(const ValueNode &tuningData,
>>> +						      const char *prop)
>>>    {
>>>    	std::vector<double> sizes =
>>>    		tuningData[prop].get<std::vector<double>>().value_or(utils::defopt);
>>> @@ -359,22 +75,36 @@ std::vector<double> parseSizes(const ValueNode &tuningData,
>>>    	return sizes;
>>>    }
>>> -unsigned int quantize(unsigned int value, unsigned int step)
>>> +/*
>>> + * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left
>>> + * quadrant of the grid. This is then mirrored in hardware to the other
>>> + * quadrants. See parseSizes() for further details. For easier handling, this
>>> + * function converts the cell sizes of half the grid to a list of position of
>>> + * the whole grid (on one axis). Example:
>>> + *
>>> + * input:   | 0.2 | 0.3 |
>>> + * output: 0.0   0.2   0.5   0.8   1.0
>>> + */
>>> +std::vector<double> LensShadingCorrection::sizesToPositions(Span<const double> sizes)
>>>    {
>>> -	return std::lround(value / static_cast<double>(step)) * step;
>>> -}
>>> +	const int half = sizes.size();
>>> +	std::vector<double> positions(half * 2 + 1);
>>> +	double x = 0.0;
>>> -} /* namespace */
>>> +	positions[half] = 0.5;
>>> +	for (int i = 1; i <= half; i++) {
>>> +		x += sizes[half - i];
>>> +		positions[half - i] = 0.5 - x;
>>> +		positions[half + i] = 0.5 + x;
>>> +	}
>>> -LensShadingCorrection::LensShadingCorrection()
>>> -	: lastAppliedCt_(0), lastAppliedQuantizedCt_(0)
>>> -{
>>> +	return positions;
>>>    }
>>>    /**
>>>     * \copydoc libcamera::ipa::Algorithm::init
>>>     */
>>> -int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
>>> +int LensShadingCorrection::init(IPAContext &context,
>>>    				const ValueNode &tuningData)
>>>    {
>>>    	xSize_ = parseSizes(tuningData, "x-size");
>>> @@ -383,56 +113,27 @@ int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
>>>    	if (xSize_.empty() || ySize_.empty())
>>>    		return -EINVAL;
>>> -	/* Get all defined sets to apply. */
>>> -	const ValueNode &yamlSets = tuningData["sets"];
>>> -	if (!yamlSets.isList()) {
>>> -		LOG(RkISP1Lsc, Error)
>>> -			<< "'sets' parameter not found in tuning file";
>>> -		return -EINVAL;
>>> -	}
>>> -
>>> -	ShadingDescriptorMap lscData;
>>> -	int ret = 0;
>>> -
>>> -	std::string type = tuningData["type"].get<std::string>("table");
>>> -	if (type == "table") {
>>> -		LOG(RkISP1Lsc, Debug) << "Loading tabular LSC data.";
>>> -		auto loader = LscTableLoader();
>>> -		ret = loader.parseLscData(yamlSets, lscData);
>>> -	} else if (type == "polynomial") {
>>> -		LOG(RkISP1Lsc, Debug) << "Loading polynomial LSC data.";
>>> -		/*
>>> -		 * \todo: Most likely the reference frame should be native_size.
>>> -		 * Let's wait how the internal discussions progress.
>>> -		 */
>>> -		auto loader = LscPolynomialLoader(context.sensorInfo.activeAreaSize);
>>> -		ret = loader.parseLscData(yamlSets, lscData);
>>> -	} else {
>>> -		LOG(RkISP1Lsc, Error) << "Unsupported LSC data type '"
>>> -				      << type << "'";
>>> -		ret = -EINVAL;
>>> -	}
>>> -
>>> -	if (ret)
>>> -		return ret;
>>> +	xPos_ = sizesToPositions(xSize_);
>>> +	yPos_ = sizesToPositions(ySize_);
>>> -	context.ctrlMap[&controls::LensShadingCorrectionEnable] =
>>> -		ControlInfo(false, true, true);
>>> -
>>> -	shadingDescriptors_ = std::move(lscData);
>>> -
>>> -	return 0;
>>> +	return lscAlgo_.init(tuningData, context.ctrlMap, {
>>> +				.keys = { "r", "gr", "gb", "b" },
>>> +				.numHCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,
>>> +				.numVCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,
>>> +				.sensorSize = context.sensorInfo.activeAreaSize
>>> +			     });
>>>    }
>>>    /**
>>>     * \copydoc libcamera::ipa::Algorithm::configure
>>>     */
>>>    int LensShadingCorrection::configure(IPAContext &context,
>>> -				     [[maybe_unused]] const IPACameraSensorInfo &configInfo)
>>> +				     const IPACameraSensorInfo &configInfo)
>>>    {
>>>    	const Size &size = context.configuration.sensor.size;
>>>    	Size totalSize{};
>>> +	/* Calculate gradients. */
>>>    	for (unsigned int i = 0; i < RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE; ++i) {
>>>    		xSizes_[i] = xSize_[i] * size.width;
>>>    		ySizes_[i] = ySize_[i] * size.height;
>>> @@ -455,16 +156,8 @@ int LensShadingCorrection::configure(IPAContext &context,
>>>    		yGrad_[i] = std::round(32768 / ySizes_[i]);
>>>    	}
>>> -	LOG(RkISP1Lsc, Debug) << "Sample LSC data for " << configInfo.analogCrop;
>>> -	std::map<unsigned int, LensShadingCorrection::Components> shadingData;
>>> -	for (const auto &[t, descriptor] : shadingDescriptors_)
>>> -		shadingData[t] = descriptor->sampleForCrop(configInfo.analogCrop,
>>> -							   xSize_, ySize_);
>>> -
>>> -	sets_.setData(std::move(shadingData));
>>> -
>>> -	context.activeState.lsc.enabled = true;
>>> -	return 0;
>>> +	return lscAlgo_.configure(context.activeState.lsc, configInfo.analogCrop,
>>> +				  xPos_, yPos_);
>>>    }
>>>    void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)
>>> @@ -476,12 +169,16 @@ void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)
>>>    }
>>>    void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
>>> -				      const Components &set)
>>> +				      const ipa::lsc::Components<uint16_t> &set)
>>>    {
>>> -	std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
>>> -	std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
>>> -	std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
>>> -	std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
>>> +	const auto &r = set.at("r");
>>> +	std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
>>> +	const auto &gr = set.at("gr");
>>> +	std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
>>> +	const auto &gb = set.at("gb");
>>> +	std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
>>> +	const auto &b = set.at("b");
>>> +	std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
>>>    }
>>>    /**
>>> @@ -492,19 +189,8 @@ void LensShadingCorrection::queueRequest(IPAContext &context,
>>>    					 IPAFrameContext &frameContext,
>>>    					 const ControlList &controls)
>>>    {
>>> -	auto &lsc = context.activeState.lsc;
>>> -
>>> -	const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);
>>> -	if (lscEnable && *lscEnable != lsc.enabled) {
>>> -		lsc.enabled = *lscEnable;
>>> -
>>> -		LOG(RkISP1Lsc, Debug)
>>> -			<< (lsc.enabled ? "Enabling" : "Disabling") << " Lsc";
>>> -
>>> -		frameContext.lsc.update = true;
>>> -	}
>>> -
>>> -	frameContext.lsc.enabled = lsc.enabled;
>>> +	lscAlgo_.queueRequest(context.activeState.lsc, frameContext.lsc,
>>> +			      controls);
>>>    }
>>>    /**
>>> @@ -542,7 +228,7 @@ void LensShadingCorrection::prepare([[maybe_unused]] IPAContext &context,
>>>    	setParameters(*config);
>>> -	const Components &set = sets_.getInterpolated(quantizedCt);
>>> +	const auto &set = lscAlgo_.interpolateComponents(quantizedCt);
>>>    	copyTable(*config, set);
>>>    	lastAppliedCt_ = ct;
>>> @@ -562,7 +248,7 @@ void LensShadingCorrection::process([[maybe_unused]] IPAContext &context,
>>>    				    [[maybe_unused]] const rkisp1_stat_buffer *stats,
>>>    				    ControlList &metadata)
>>>    {
>>> -	metadata.set(controls::LensShadingCorrectionEnable, frameContext.lsc.enabled);
>>> +	lscAlgo_.process(frameContext.lsc, metadata);
>>>    }
>>>    REGISTER_IPA_ALGORITHM(LensShadingCorrection, "LensShadingCorrection")
>>> diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
>>> index 0a256e225327..7c656d95acfb 100644
>>> --- a/src/ipa/rkisp1/algorithms/lsc.h
>>> +++ b/src/ipa/rkisp1/algorithms/lsc.h
>>> @@ -2,17 +2,23 @@
>>>    /*
>>>     * Copyright (C) 2021-2022, Ideas On Board
>>>     *
>>> - * RkISP1 Lens Shading Correction control
>>> + * RkISP1 Lens Shading Correction algorithm
>>>     */
>>>    #pragma once
>>> -#include <map>
>>> -#include <memory>
>>> +#include <vector>
>>> -#include "libipa/interpolator.h"
>>> +#include <linux/rkisp1-config.h>
>>> +
>>> +#include "libcamera/internal/value_node.h"
>>> +
>>> +#include "libipa/fixedpoint.h"
>>> +#include "libipa/lsc.h"
>>>    #include "algorithm.h"
>>> +#include "ipa_context.h"
>>> +#include "params.h"
>>>    namespace libcamera {
>>> @@ -37,39 +43,28 @@ public:
>>>    		     const rkisp1_stat_buffer *stats,
>>>    		     ControlList &metadata) override;
>>> -	struct Components {
>>> -		std::vector<uint16_t> r;
>>> -		std::vector<uint16_t> gr;
>>> -		std::vector<uint16_t> gb;
>>> -		std::vector<uint16_t> b;
>>> -	};
>>> -
>>> -	class ShadingDescriptor
>>> -	{
>>> -	public:
>>> -		virtual ~ShadingDescriptor() = default;
>>> -		virtual Components sampleForCrop(const Rectangle &cropRectangle,
>>> -						 Span<const double> xSizes,
>>> -						 Span<const double> ySizes) = 0;
>>> -	};
>>> -
>>> -	using ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;
>>> -
>>>    private:
>>> +	std::vector<double> parseSizes(const ValueNode &tuningData,
>>> +				       const char *prop);
>>> +	std::vector<double> sizesToPositions(Span<const double> sizes);
>>> +
>>>    	void setParameters(rkisp1_cif_isp_lsc_config &config);
>>> -	void copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);
>>> +	void copyTable(rkisp1_cif_isp_lsc_config &config,
>>> +		       const ipa::lsc::Components<uint16_t> &set0);
>>> -	ShadingDescriptorMap shadingDescriptors_;
>>> -	ipa::Interpolator<Components> sets_;
>>>    	std::vector<double> xSize_;
>>>    	std::vector<double> ySize_;
>>>    	uint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
>>>    	uint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
>>>    	uint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
>>>    	uint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
>>> +	std::vector<double> xPos_;
>>> +	std::vector<double> yPos_;
>>>    	unsigned int lastAppliedCt_;
>>>    	unsigned int lastAppliedQuantizedCt_;
>>> +
>>> +	LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;
>>>    };
>>>    } /* namespace ipa::rkisp1::algorithms */
>>> diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
>>> index cd97e10bcf2b..005f4102b4f6 100644
>>> --- a/src/ipa/rkisp1/ipa_context.h
>>> +++ b/src/ipa/rkisp1/ipa_context.h
>>> @@ -30,6 +30,7 @@
>>>    #include "libipa/ccm.h"
>>>    #include "libipa/fc_queue.h"
>>>    #include "libipa/fixedpoint.h"
>>> +#include "libipa/lsc.h"
>>>    namespace libcamera {
>>> @@ -138,9 +139,7 @@ struct IPAActiveState {
>>>    		double strength;
>>>    	} wdr;
>>> -	struct {
>>> -		bool enabled;
>>> -	} lsc;
>>> +	ipa::lsc::ActiveState lsc;
>>>    };
>>>    struct IPAFrameContext : public FrameContext {
>>> @@ -213,10 +212,7 @@ struct IPAFrameContext : public FrameContext {
>>>    		double gain;
>>>    	} wdr;
>>> -	struct {
>>> -		bool enabled;
>>> -		bool update;
>>> -	} lsc;
>>> +	ipa::lsc::FrameContext lsc;
>>>    };
>>>    struct IPAContext {
>>>
>>
Stefan Klug June 26, 2026, 9:18 a.m. UTC | #5
Hi Jacopo,

Thank you for the refactoring.

Quoting Jacopo Mondi (2026-06-15 16:05:34)
> Introduce an Lsc algorithm implementation in libIPA.
> 
> The LscAlgorithm class supports two algorithms: grid-based lsc and
> polynomial-based lsc.
> 
> Both implementations have been generalized from the existing ones in the
> RkISP1 IPA.
> 
> Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> ---
>  src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++
>  src/ipa/libipa/lsc.h              | 145 +++++++++++++
>  src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++
>  src/ipa/libipa/lsc_base.h         |  59 ++++++
>  src/ipa/libipa/lsc_grid.cpp       |  33 +++
>  src/ipa/libipa/lsc_grid.h         | 113 ++++++++++
>  src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-
>  src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----
>  src/ipa/libipa/meson.build        |   5 +
>  src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------
>  src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--
>  src/ipa/rkisp1/ipa_context.h      |  10 +-
>  12 files changed, 1126 insertions(+), 450 deletions(-)
> 
> diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
> new file mode 100644
> index 000000000000..ae713a776778
> --- /dev/null
> +++ b/src/ipa/libipa/lsc.cpp
> @@ -0,0 +1,322 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026 Ideas on Board Oy
> + *
> + * libIPA Lsc algorithms
> + */
> +
> +#include "lsc.h"
> +
> +#include <libcamera/base/log.h>
> +
> +/**
> + * \file lsc.h
> + * \brief libipa lsc algorithm
> + */
> +
> +namespace libcamera {
> +
> +LOG_DEFINE_CATEGORY(Lsc)
> +
> +namespace ipa {
> +
> +namespace lsc {
> +
> +/**
> + * \struct ActiveState
> + * \brief The lsc active state
> + *
> + * \var ActiveState::enabled
> + * \brief Boolean flag for the LscAlgorithm enable status
> + */
> +
> +/**
> + * \struct FrameContext
> + * \brief The lsc frame context
> + *
> + * \var FrameContext::enabled
> + * \brief Boolean flag for the LscAlgorithm enable status
> + *
> + * \var FrameContext::update
> + * \brief Boolean flag for the LscAlgorithm updated status
> + */
> +
> +} /* namespace lsc */
> +
> +#ifndef __DOXYGEN__
> +template<>
> +void Interpolator<lsc::Components<uint16_t>>::
> +       interpolate(const lsc::Components<uint16_t> &a,
> +                   const lsc::Components<uint16_t> &b,
> +                   lsc::Components<uint16_t> &dest,
> +                   double lambda)
> +{
> +       for (auto const &[k, v] : a)
> +               interpolateVector(v, b.at(k), dest[k], lambda);
> +}
> +#endif
> +
> +/**
> + * \class LscAlgorithmBase
> + * \brief Base class for LscAlgorithm
> + *
> + * Base class for LscAlgorithm for non-templated functions implementation
> + */
> +
> +/**
> + * \brief Queue a request to the lsc algorithm
> + * \param[in] state The lsc active state
> + * \param[in] context The lsc frame context
> + * \param[in] controls The list of controls associated with a Request
> + *
> + * Queue a new list of \a controls to the lsc algorithm.
> + * The only supported control is controls::LensShadingCorrectionEnable.
> + */
> +void LscAlgorithmBase::queueRequest(lsc::ActiveState &state,
> +                                   lsc::FrameContext &context,
> +                                   const ControlList &controls)
> +{
> +       const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);
> +       if (lscEnable && *lscEnable != state.enabled) {
> +               state.enabled = *lscEnable;
> +
> +               LOG(Lsc, Debug)
> +                       << (state.enabled ? "Enabling" : "Disabling") << " Lsc";
> +
> +               context.update = true;
> +       }
> +
> +       context.enabled = state.enabled;
> +}
> +
> +/**
> + * \brief Populate the list of lsc metadata
> + * \param[in] context The lsc frame context
> + * \param[in] metadata The list of metadata
> + *
> + * Populates the list of \a metadata with controls handled by the LscAlgorithm
> + * class. The only supported metadata is controls::LensShadingCorrectionEnable.
> + */
> +void LscAlgorithmBase::process(lsc::FrameContext &context, ControlList &metadata)
> +{
> +       metadata.set(controls::LensShadingCorrectionEnable, context.enabled);
> +}
> +
> +/**
> + * \class LscAlgorithm
> + * \brief libIPA lsc algorithm implementation
> + * \tparam T The type used to store the gain values when loaded from tuning file
> + * \tparam U The fixedpoint lsc engine register format
> + *
> + * Due to the optical characteristics of the lens, the light intensity received
> + * by the sensor is not uniform. The Lens Shading Correction algorithm applies
> + * multipliers to all pixels to compensate for the lens shading effect.
> + *
> + * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.
> + * It provides support for parsing the tuning file content and generate tables
> + * of per-colour temperature gains that IPA algorithms can use to program their
> + * lsc engine.
> + *
> + * The init() function parses the tuning file and loads the gain tables either
> + * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain
> + * tables are organized per-colour temperature with per-colour components gain
> + * vectors or polynomial coefficients. The colour components names are
> + * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc
> + * engine support 4 colour components (r, gr, gb, b), some only support 3 colour
> + * components (r, g, b). The name (and number) of the expected colour components
> + * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys
> + * field.
> + *
> + * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour
> + * components. The gain table has been here omitted, but the expected number

Nit: s/here omitted/omitted here/

> + * of entries has be equal to

Nit: has to be

> + * LscDescriptor::numHCells * LscDescriptor::numVCells.
> + *
> + * \code{.yaml}
> + * - Lsc:
> + *    sets:
> + *      - ct: 2500
> + *        r: [
> + *             .. gains table omitted..
> + *        ]
> + *        g: [
> + *             .. gains table omitted..
> + *        ]
> + *        b: [
> + *             .. gains table omitted..
> + *        ]
> + *      - ct: 6500
> + *        r: [
> + *             .. gains table omitted..
> + *        ]
> + *        g: [
> + *             .. gains table omitted..
> + *        ]
> + *        b: [
> + *             .. gains table omitted..
> + *        ]
> + * \endcode
> + *
> + * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'
> + * colour components:
> + *
> + * \code{.yaml}
> + * - Lsc:
> + *    type: "polynomial"
> + *    sets:
> + *      - ct: 2500
> + *        r:
> + *          cx: 0.5006571711950275
> + *          cy: 0.510093737499277
> + *          k0: 1.5393282208428813
> + *          k1: -1.1434559757908016
> + *          k2: 4.332602305814554
> + *          k3: 0.0
> + *          k4: 0.0
> + *        gr:
> + *          cx: 0.5009320529087338
> + *          cy: 0.511208038949085
> + *          k0: 1.5634738574805407
> + *          k1: -1.5623484259968348
> + *          k2: 4.846686073656501
> + *          k3: 0.0
> + *          k4: 0.0
> + *        gb:
> + *          cx: 0.5012013290343839
> + *          cy: 0.5128251541578288
> + *          k0: 1.526147944919103
> + *          k1: -1.4316976083689723
> + *          k2: 4.792604063222728
> + *          k3: 0.0
> + *          k4: 0.0
> + *        b:
> + *          cx: 0.49864139511067784
> + *          cy: 0.5162095081739346
> + *          k0: 1.0405245474038738
> + *          k1: 0.05618339879447103
> + *          k2: 1.8792813594001752
> + *          k3: 0.0
> + *          k4: 0.0
> + *      - ct: 6000
> + *        r:
> + *          cx: 0.5006202239353942
> + *          cy: 0.5099531318307661
> + *          k0: 1.4702946023945032
> + *          k1: -0.8893767547927631
> + *          k2: 3.920547732201387
> + *          k3: 0.0
> + *          k4: 0.0
> + *        gr:
> + *          cx: 0.500907874178317
> + *          cy: 0.511084916024106
> + *          k0: 1.5336172760559457
> + *          k1: -1.39964026514435
> + *          k2: 4.565487728954618
> + *          k3: 0.0
> + *          k4: 0.0
> + *        gb:
> + *          cx: 0.5011898608900477
> + *          cy: 0.5126797906745105
> + *          k0: 1.5013145790354843
> + *          k1: -1.2747407173754124
> + *          k2: 4.514682876897286
> + *          k3: 0.0
> + *          k4: 0.0
> + *        b:
> + *          cx: 0.4987561413116136
> + *          cy: 0.5159619420778772
> + *          k0: 1.0102986422191802
> + *          k1: 0.13263449763985727
> + *          k2: 1.686556107316064
> + *          k3: 0.0
> + *          k4: 0.0
> + * \endcode
> + *
> + * The lsc tables or the polynomial definition are generated at tuning time
> + * using an image of known resolution which needs to be specified in
> + * LscDescriptor::sensorSize.
> + *
> + * At LscAlgorithm::configure() time the lsc tables are re-sampled on the
> + * sensor's crop rectangle in use to adapt them to the configuration in use for
> + * a streaming session. Polynomial lsc tables support re-sampling and can be
> + * applied to any sensor configuration. Grid-based lsc tables cannot be
> + * re-sampled and the configuration as parsed from the tuning file is used for
> + * all sensor configurations providing best-effort results.
> + *
> + * When the IPA algorithms wants to get access to the (resampled) tables to
> + * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get
> + * an lsc table interpolated by the LscAlgorithm class for the specified colour
> + * temperature. If the algorithm wants to access the non-interpolated tables it
> + * can retrieve them using LscAlgorithm::getComponents().
> + */
> +
> +/**
> + * \fn LscAlgorithm::init()
> + * \param[in] tuningData The tuning data
> + * \param[in] controls The IPA list of supported controls
> + * \param[in] descriptor The lsc engine descriptor
> + *
> + * Parse \a tuningData according to the settings specified in \a descriptor to
> + * populate the lsc data and registers lsc controls in \a controls.
> + *
> + * \return 0 on success, a negative error code otherwise
> + */
> +
> +/**
> + * \fn LscAlgorithm::configure()
> + * \param[in] state The lsc active state
> + * \param[in] analogCrop The current sensor analog crop rectangle
> + * \param[in] xPos List of horizontal positions of the LSC grid nodes
> + * \param[in] yPos List of vertical positions of the LSC grid nodes
> + *
> + * Re-sample the lsc data for an \a analogCrop.
> + *
> + * Lsc data are generated at tuning time using a known sensor configuration.
> + * When a new streaming session is started, it might use a different sensor
> + * configuration for which the lsc tables need to be adjusted to.
> + *
> + * This function re-generates the lsc tables to adapt them to a new sensor
> + * configuration, specifically it re-samples the lsc data for a new \a
> + * analogCrop on a grid specified by \a xPos and \a yPos. Re-sampling of
> + * lsc data is only supported by polynomial-based lsc tables.
> + *
> + * \sa LscImplementation::resampleLscData
> + *
> + * \return 0 on success, a negative error code otherwise
> + */
> +
> +/**
> + * \fn LscAlgorithm::interpolateComponents
> + * \brief Interpolate the lsc tables for a given colour temperature
> + * \param[in] ct The colour temperature
> + *
> + * Lsc data are generated using different colour temperatures during the
> + * tuning phase.
> + *
> + * This function returns the interpolated lsc data for a given \a ct
> + * colour temperature.
> + *
> + * IPA algorithm can use this function to obtain a list of gains per-colour
> + * component to program their lsc engines with every time a significant enough
> + * change in colour temperature is detected.
> + *
> + * Calling this function is only valid after LscAlgorithm::configure() has been
> + * called. An empty components list is returned otherwise.
> + *
> + * \return The lsc gains table interpolated for temperature \a ct
> + */
> +
> +/**
> + * \fn LscAlgorithm::getComponents
> + *
> + * Return the map of lsc data per colour temperature.
> + *
> + * Calling this function is only valid after LscAlgorithm::configure() has been
> + * called. An empty components list is returned otherwise.
> + *
> + * \return The map of lsc gains tables per colour-temperature
> + */
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc.h b/src/ipa/libipa/lsc.h
> new file mode 100644
> index 000000000000..8bfa3a7c4ecf
> --- /dev/null
> +++ b/src/ipa/libipa/lsc.h
> @@ -0,0 +1,145 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026 Ideas on Board Oy
> + *
> + * libIPA Lsc algorithms
> + */
> +
> +#pragma once
> +
> +#include <memory>
> +
> +#include <libcamera/control_ids.h>
> +
> +#include "interpolator.h"
> +#include "lsc_grid.h"
> +#include "lsc_polynomial.h"
> +
> +namespace libcamera {
> +
> +LOG_DECLARE_CATEGORY(Lsc)
> +
> +namespace ipa {
> +
> +namespace lsc {
> +
> +struct ActiveState {
> +       bool enabled;
> +};
> +
> +struct FrameContext {
> +       bool enabled;
> +       bool update;
> +};
> +
> +} /* namespace lsc */
> +
> +#ifndef __DOXYGEN__
> +template<typename T>
> +void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,
> +                      std::vector<T> &dest, double lambda)
> +{
> +       ASSERT(a.size() == b.size());
> +       dest.resize(a.size());
> +       for (size_t i = 0; i < a.size(); i++)
> +               dest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;
> +}
> +
> +template<>
> +void Interpolator<lsc::Components<uint16_t>>::
> +       interpolate(const lsc::Components<uint16_t> &a,
> +                   const lsc::Components<uint16_t> &b,
> +                   lsc::Components<uint16_t> &dest,
> +                   double lambda);
> +#endif /* __DOXYGEN__ */
> +
> +class LscAlgorithmBase
> +{
> +public:
> +       void queueRequest(lsc::ActiveState &state, lsc::FrameContext &context,
> +                         const ControlList &controls);
> +       void process(lsc::FrameContext &context, ControlList &metadata);
> +};
> +
> +template<typename T, typename U>
> +class LscAlgorithm : public LscAlgorithmBase
> +{
> +public:
> +       int init(const ValueNode &tuningData, ControlInfoMap::Map &controls,
> +                LscDescriptor descriptor)
> +       {
> +               polynomial_ = false;
> +
> +               std::string type = tuningData["type"].get<std::string>("table");
> +               if (type == "table") {
> +                       impl_ = std::make_unique<LscGrid<T, U>>();
> +                       LOG(Lsc, Debug) << "Using grid-based Lsc";
> +               } else if (type == "polynomial") {
> +                       impl_ = std::make_unique<LscPolynomial<T, U>>();
> +                       polynomial_ = true;
> +                       LOG(Lsc, Debug) << "Using polynomial Lsc";
> +               } else {
> +                       LOG(Lsc, Error) << "Unsupported Lsc algorithm '"
> +                                       << type << "'";
> +                       return -EINVAL;
> +               }
> +
> +               const ValueNode &yamlSets = tuningData["sets"];
> +               if (!yamlSets.isList()) {
> +                       LOG(Lsc, Error) << "'sets' parameter not found in tuning file";
> +                       return -EINVAL;
> +               }
> +
> +               int ret = impl_->parseLscData(yamlSets, descriptor);
> +               if (ret)
> +                       return ret;
> +
> +               controls[&controls::LensShadingCorrectionEnable] =
> +                       ControlInfo(false, true, true);
> +
> +               return 0;
> +       }
> +
> +       int configure(lsc::ActiveState &state, const Rectangle &analogCrop,
> +                     const std::vector<double> &xPos,
> +                     const std::vector<double> &yPos)
> +       {
> +               LOG(Lsc, Debug) << "Sample Lsc data for " << analogCrop;
> +               lsc::ComponentsMap<T> lscData =
> +                       impl_->resampleLscData(analogCrop, xPos, yPos);
> +
> +               /*
> +                * Retain a copy of the components table.
> +                *
> +                * We could avoid a copy here if getComponents() could
> +                * return sets_.data() but I wasn't able to work around the
> +                * compiler refusing it.
> +                */
> +               lscData_ = lscData;
> +
> +               sets_.setData(std::move(lscData));
> +               state.enabled = true;
> +
> +               return 0;
> +       }
> +
> +       const lsc::Components<T> interpolateComponents(unsigned int ct)
> +       {
> +               return sets_.getInterpolated(ct);
> +       }
> +
> +       const lsc::ComponentsMap<T> getComponents()
> +       {
> +               return lscData_;
> +       }
> +
> +private:
> +       std::unique_ptr<LscImplementation<T, U>> impl_;
> +       Interpolator<lsc::Components<T>> sets_;
> +       lsc::ComponentsMap<T> lscData_;
> +       bool polynomial_;
> +};
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_base.cpp b/src/ipa/libipa/lsc_base.cpp
> new file mode 100644
> index 000000000000..fba8d2f00d54
> --- /dev/null
> +++ b/src/ipa/libipa/lsc_base.cpp
> @@ -0,0 +1,152 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026 Ideas on Board Oy
> + *
> + * Base classes and types for LSC algorithms implementations
> + */
> +
> +#include "lsc_base.h"
> +
> +/**
> + * \file lsc_base.h
> + * \brief Base types and definitions for LscImplementation class hierarchy
> + *
> + * Split the common classes and types definitions to a dedicated file to avoid
> + * circular inclusions.
> + *
> + * Both LscGrid and LscPolynomial inherit from LscImplementation and they are
> + * instantiated from lsc.h as the LscAlgorithm::init() function is there inlined.
> + *
> + * For this reason lsc.h needs to include lsc_grid.h and lsc_polynomial.h which
> + * need the LscImplementation class definition themselves.
> + *
> + * Split the LscImplementation interface definition to this file so that it can
> + * be included by both lsc_grid.h and lsc_polynomial.h, which are then included
> + * by lsc.h.
> + */
> +
> +namespace libcamera {
> +
> +namespace ipa {
> +
> +namespace lsc {
> +
> +/**
> + * \class Components
> + * \brief Associate a colour components with a list of gains
> + * \tparam T The type used to store the gain values when loaded from tuning file
> + *
> + * Lsc tables are defined as a list of gain values associated to a colour
> + * component.
> + *
> + * As different ISP support different colour components (usually 'r', 'gr',
> + * 'gb', 'b' or just 'r', 'g', 'b') this class associates a string
> + * identifier for the colour component to a list of gains of type \a T.
> + *
> + * Each key name shall match an entry in the tuning file and the type \a T
> + * shall match the size of the registers where gains are stored.
> + *
> + * The list of keys is provided to the LscAlgorithm class using \a
> + * LscDescriptor::keys.
> + */
> +
> +/**
> + * \class ComponentsMap
> + * \brief Associate a colour temperature to a lsc table
> + * \tparam T The type used to store the gain values when loaded from tuning file
> + *
> + * An lsc table is generated during the tuning phase for a specific light
> + * temperature, and a tuning file usually contains lsc tables generated for
> + * several different colour temperatures.
> + *
> + * This class associates an lsc table to the colour temperature used when the
> + * table has been generated.
> + */
> +
> +} /* namespace lsc */
> +
> +/**
> + * \struct LscDescriptor
> + * \brief Describe the ISP lsc engine
> + *
> + * \var LscDescriptor::keys
> + * \brief The list of colour components to which a list of gains is associated
> + * with in the tuning file. Used for parsing the tuning file
> + *
> + * \var LscDescriptor::numHCells
> + * \brief Number of horizontal cells of the ISP lsc grid. Used for validating
> + * the list of gains parsed from tuning file
> + *
> + * \var LscDescriptor::numVCells
> + * \brief Number of vertical cells of the ISP lsc grid. Used for validating
> + * the list of gains parsed from tuning file
> + *
> + * \var LscDescriptor::sensorSize
> + * \brief The physical sensor size. This is the largest frame size used to
> + * generate the lsc table. Only used by the polynomial lsc algorithm
> + */
> +
> +/**
> + * \class LscImplementation
> + * \brief Pure virtual base class for lsc algorithm implementations
> + * \tparam T The type used to store the gain values when loaded from tuning file
> + * \tparam U The fixedpoint format used to convert gain values generated by
> + * polynomial expansion to the register format
> + *
> + * Defines the interface for the lsc algorithm implementation. Currently
> + * implemented by LscGrid and LscPolynomial.
> + */
> +
> +/**
> + * \fn LscImplementation::~LscImplementation
> + * \brief Virtual class destructor
> + */
> +
> +/**
> + * \fn LscImplementation::parseLscData
> + * \brief Parse \a tuningData using \a descriptor
> + * \param[in] tuningData The tuning data
> + * \param[in] descriptor The lsc engine descriptor
> + *
> + * Parse the tuning file using the \a descriptor to identify the colour
> + * components in the tuning data and validate the size of the loaded gains
> + * tables.
> + *
> + * \return 0 on success, a negative error number otherwise
> + */
> +
> +/**
> + * \fn LscImplementation::resampleLscData
> + * \brief Re-sample the lsc components for \a cropRectangle
> + * \param[in] cropRectangle The sensor analogue crop rectangle
> + * \param[in] xPos List of horizontal positions of the lsc grid nodes
> + * \param[in] yPos List of vertical positions of the lsc grid nodes
> + *
> + * Lsc tables are expressed in two formats:
> + * - A list of gain values (LscGrid)
> + * - A radial polynomial (LscPolynomial)
> + *
> + * Grid-based lsc data are generated using an image at a fixed resolution and
> + * can't at the moment be re-sampled when a different resolution is used for a
> + * streaming session. Re-sampling a grid lsc table will return the same table
> + * as loaded from the tuning file.
> + *
> + * Polynomial are more flexible and can be re-sampled for a given resolution
> + * using a list of horizontal and vertical nodes that define the lsc grid.
> + * Polynomial lsc tables have to be re-sampled every time a new configuration is
> + * applied, as each streaming session might use a different sensor crop
> + * rectangle.
> + *
> + * \a cropRectangle represents the size of the frame on which the polynomial Lsc
> + * has to be re-sampled on.
> + *
> + * \a xPos and \a yPos represent the position of the grid nodes vertexes in
> + * the [0, 1] interval. In example an equally spaced grid of 16 nodes will have
> + * each segment of size 0.0625 and the list of nodes position will be
> + * [0, 0.0625, 0.125, 0.1875, ... , 1]. It is expected that the first position
> + * is 0 and the last position is 1.
> + */
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
> new file mode 100644
> index 000000000000..aecae305aa9c
> --- /dev/null
> +++ b/src/ipa/libipa/lsc_base.h
> @@ -0,0 +1,59 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026 Ideas on Board Oy
> + *
> + * Base classes and types for LSC algorithms implementations
> + */
> +
> +#pragma once
> +
> +#include <map>
> +#include <string>
> +#include <vector>
> +
> +#include <libcamera/geometry.h>
> +
> +#include "libcamera/internal/value_node.h"
> +
> +namespace libcamera {
> +
> +namespace ipa {
> +
> +namespace lsc {
> +
> +template<typename T>
> +class Components : public std::map<std::string, std::vector<T>>
> +{

I'm a bit uneasy here. On the one hand we are going great lengths to
write super optimized code striving for static allocations as much as
possible and now we add a string based map lookup that is run per frame.
On the other hand I don't actually know how expensive the lookup is.

I was wondering if it is possible to template that using a static set as
temüplate param and a fixed size array as container but then you can't
configure that at init() time and end up with too much template
magic....no

As a compromise, would it make sense to index the map by an enum, so
that we could define an enum for the possible types (it will most likely
ever only be g,r,b,rg,bg (possible ir in the future). That would at
least replace the string lookup by an int lookup, and we'd only have
strings at yaml parse time?

> +};
> +
> +template<typename T>
> +class ComponentsMap : public std::map<unsigned int, Components<T>>
> +{
> +};
> +
> +} /* namespace lsc */
> +
> +struct LscDescriptor {
> +       std::vector<std::string> keys;
> +       unsigned int numHCells;
> +       unsigned int numVCells;
> +       Size sensorSize;
> +};
> +
> +template<typename T, typename U>
> +class LscImplementation

On v1 Dan commented that U is not used in grid based. But it is used in
the polynomial case. What about dropping T and defining it as
U::QuantizedType? That would work for both cases and you get rid of one
parameter. (And if there is a hardware that has a different register
size than U::QuantizedType, the current solution would rely on luck,
too.)

> +{
> +public:
> +       virtual ~LscImplementation() = default;
> +
> +       virtual int parseLscData(const ValueNode &tuningData,
> +                                const LscDescriptor &descriptor) = 0;
> +
> +       virtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> +                                                     const std::vector<double> &xPos,
> +                                                     const std::vector<double> &yPos) = 0;
> +};
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_grid.cpp b/src/ipa/libipa/lsc_grid.cpp
> new file mode 100644
> index 000000000000..878994acc8c9
> --- /dev/null
> +++ b/src/ipa/libipa/lsc_grid.cpp
> @@ -0,0 +1,33 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026, Ideas On Board
> + *
> + * Grid based lens shading correction
> + */
> +
> +#include "lsc_grid.h"
> +
> +/**
> + * \file lsc_grid.h
> + * \brief LscGrid class
> + */
> +
> +namespace libcamera {
> +
> +LOG_DEFINE_CATEGORY(LscGrid)
> +
> +namespace ipa {
> +
> +/**
> + * \class LscGrid
> + * \brief Grid based lsc algorithm implementation
> + *
> + * Grid based lsc algorithm implementation. The LscGrid class implements lsc
> + * support using tabular lsc data.
> + *
> + * \sa LscImplementation
> + * \sa LscAlgorithm
> + */
> +
> +} /* namespace ipa */
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_grid.h b/src/ipa/libipa/lsc_grid.h
> new file mode 100644
> index 000000000000..e92cde95d18d
> --- /dev/null
> +++ b/src/ipa/libipa/lsc_grid.h
> @@ -0,0 +1,113 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026, Ideas On Board
> + *
> + * Grid based lens shading correction
> + */
> +
> +#pragma once
> +
> +#include <vector>
> +
> +#include <libcamera/base/log.h>
> +
> +#include "lsc_base.h"
> +
> +namespace libcamera {
> +
> +LOG_DECLARE_CATEGORY(LscGrid)
> +
> +namespace ipa {
> +
> +template<typename T, typename U>
> +class LscGrid : public LscImplementation<T, U>
> +{
> +public:
> +       ~LscGrid() {}
> +
> +       int parseLscData(const ValueNode &yamlSets,
> +                        const LscDescriptor &descriptor) override
> +       {
> +               const auto &sets = yamlSets.asList();
> +
> +               for (const auto &yamlSet : sets) {
> +                       uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> +
> +                       int ret = parseLscComponent(yamlSet, ct, descriptor);
> +                       if (ret)
> +                               return ret;
> +               }
> +
> +               if (lscData_.empty()) {
> +                       LOG(LscGrid, Error) << "Failed to load any sets";
> +                       return -EINVAL;
> +               }
> +
> +               return 0;
> +       }
> +
> +       lsc::ComponentsMap<T> resampleLscData([[maybe_unused]] const Rectangle &cropRectangle,
> +                                             [[maybe_unused]] const std::vector<double> &xPos,
> +                                             [[maybe_unused]] const std::vector<double> &yPos) override
> +       {
> +               /* No resampling for grid-based LSC algorithm. */
> +               return lscData_;
> +       }
> +
> +private:
> +       int parseLscComponent(const ValueNode &yamlSet,
> +                             unsigned int ct, const LscDescriptor &descriptor)
> +       {
> +               lsc::Components<T> component;
> +               for (auto &k : descriptor.keys) {
> +                       auto [it, inserted] = component.emplace(
> +                               std::piecewise_construct,
> +                               std::forward_as_tuple(k.c_str()),
> +                               std::forward_as_tuple(parseTable(yamlSet,
> +                                                                k.c_str(),
> +                                                                descriptor.numHCells,
> +                                                                descriptor.numVCells)));
> +                       if (!inserted || it->second.empty()) {
> +                               LOG(LscGrid, Error)
> +                                       << "Set " << k << " for color temperature "
> +                                       << ct << " is missing";
> +                               return -EINVAL;
> +                       }
> +               }
> +
> +               auto [it, inserted] = lscData_.emplace(ct, component);
> +               if (!inserted) {
> +                       LOG(LscGrid, Error)
> +                               << "Multiple sets found for color temperature "
> +                               << ct;
> +                       return -EINVAL;
> +               }
> +
> +               return 0;
> +       }
> +
> +       std::vector<T> parseTable(const ValueNode &tuningData,
> +                                 const char *prop, unsigned int numHCells,
> +                                 unsigned int numVCells)
> +       {
> +               unsigned int kLscNumSamples = numHCells * numVCells;
> +
> +               std::vector<T> table =
> +                       tuningData[prop].get<std::vector<T>>().value_or(utils::defopt);
> +               if (table.size() != kLscNumSamples) {
> +                       LOG(LscGrid, Error)
> +                               << "Invalid '" << prop << "' values: expected "
> +                               << kLscNumSamples
> +                               << " elements, got " << table.size();
> +                       return {};
> +               }
> +
> +               return table;
> +       }
> +
> +       lsc::ComponentsMap<T> lscData_;
> +};
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
> index f607d86c54c3..8d3f1883d6a5 100644
> --- a/src/ipa/libipa/lsc_polynomial.cpp
> +++ b/src/ipa/libipa/lsc_polynomial.cpp
> @@ -1,12 +1,14 @@
>  /* SPDX-License-Identifier: LGPL-2.1-or-later */
>  /*
> - * Copyright (C) 2024, Ideas On Board
> + * Copyright (C) 2026, Ideas On Board
>   *
> - * Polynomial class to represent lens shading correction
> + * Polynomial based lens shading correction
>   */
>  
>  #include "lsc_polynomial.h"
>  
> +#include <assert.h>
> +
>  #include <libcamera/base/log.h>
>  
>  /**
> @@ -21,7 +23,7 @@ LOG_DEFINE_CATEGORY(LscPolynomial)
>  namespace ipa {
>  
>  /**
> - * \class LscPolynomial
> + * \class Polynomial
>   * \brief Class for handling even polynomials used in lens shading correction
>   *
>   * Shading artifacts of camera lenses can be modeled using even radial
> @@ -31,7 +33,7 @@ namespace ipa {
>   */
>  
>  /**
> - * \fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> + * \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
>                       double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
>                       double k4 = 0.0)
>   * \brief Construct a polynomial using the given coefficients
> @@ -45,7 +47,13 @@ namespace ipa {
>   */
>  
>  /**
> - * \fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)
> + * \fn Polynomial::Polynomial(const Polynomial &other)
> + * \brief Construct a Polynomial by copy
> + * \param[in] other The Polynomial to copy construct from
> + */
> +
> +/**
> + * \fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)
>   * \brief Sample the polynomial at the given normalized pixel position
>   *
>   * This functions samples the polynomial at the given pixel position divided by
> @@ -55,9 +63,21 @@ namespace ipa {
>   * \param y y position in normalized coordinates
>   * \return The sampled value
>   */
> +double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
> +{
> +       double dx = x - cnx_;
> +       double dy = y - cny_;
> +       double r = sqrt(dx * dx + dy * dy);
> +       double res = 1.0;
> +
> +       for (unsigned int i = 0; i < coefficients_.size(); i++)
> +               res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> +
> +       return res;
> +}

These are plain refactorings. I see that the whole libipa rework
is difficult to do in small patches, but I think it would make review
(and later history reading) easier if we'd cut out these refactorings
into separate preparatory patches.

>  
>  /**
> - * \fn LscPolynomial::getM()
> + * \fn Polynomial::getM()
>   * \brief Get the value m as described in the dng specification
>   *
>   * Returns m according to dng spec. m represents the Euclidean distance
> @@ -66,9 +86,18 @@ namespace ipa {
>   *
>   * \return The sampled value
>   */
> +double Polynomial::getM() const
> +{
> +       double cpx = imageSize_.width * cx_;
> +       double cpy = imageSize_.height * cy_;
> +       double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
> +       double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
> +
> +       return sqrt(mx * mx + my * my);
> +}
>  
>  /**
> - * \fn LscPolynomial::setReferenceImageSize(const Size &size)
> + * \fn Polynomial::setReferenceImageSize(const Size &size)
>   * \brief Set the reference image size
>   *
>   * Set the reference image size that is used for subsequent calls to getM() and
> @@ -76,6 +105,92 @@ namespace ipa {
>   *
>   * \param size The size of the reference image
>   */
> +void Polynomial::setReferenceImageSize(const Size &size)
> +{
> +       assert(!size.isNull());
> +       imageSize_ = size;
> +
> +       /* Calculate normalized centers */
> +       double m = getM();
> +       cnx_ = (size.width * cx_) / m;
> +       cny_ = (size.height * cy_) / m;
> +}
> +
> +/**
> + * \class LscPolynomialBase
> + * \brief Base class for LscPolynomial
> + *
> + * Base class for LscPolynomial for non-templated functions.
> + */
> +
> +/**
> + * \brief Parse polynomial lsc data
> + * \param[in] yamlSets The tuning file content
> + * \param[in] descriptor The lsc engine descriptor
> + *
> + * Parse the lsc data in polyomial form from the \a yamlSet tuning data.
> + */
> +int LscPolynomialBase::parseLscData(const ValueNode &yamlSets,
> +                                   const LscDescriptor &descriptor)
> +{
> +       const auto &sets = yamlSets.asList();
> +       for (const auto &yamlSet : sets) {
> +               uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> +
> +               Components components;
> +               for (auto &k : descriptor.keys) {
> +                       auto polynomial = yamlSet[k.c_str()].get<Polynomial>();
> +                       if (!polynomial) {
> +                               LOG(LscPolynomial, Error)
> +                                       << "Missing polynomial for component "
> +                                       << k;
> +                               return -EINVAL;
> +                       }
> +
> +                       auto [it, inserted] =
> +                               components.emplace(std::piecewise_construct,
> +                                                  std::forward_as_tuple(k.c_str()),
> +                                                  std::forward_as_tuple(*polynomial));
> +
> +                       it->second.setReferenceImageSize(descriptor.sensorSize);
> +               }
> +
> +               auto [it, inserted] = lscData_.emplace(ct, components);
> +               if (!inserted) {
> +                       LOG(LscPolynomial, Error)
> +                               << "Multiple sets found for "
> +                               << "color temperature " << ct;
> +                       return -EINVAL;
> +               }
> +       }
> +
> +       if (lscData_.empty()) {
> +               LOG(LscPolynomial, Error) << "Failed to load any sets";
> +               return -EINVAL;
> +       }
> +
> +       return 0;
> +}
> +
> +/**
> + * \var LscPolynomialBase::lscData_
> + * \brief The polynomial lsc data
> + *
> + * Maps colour temperatures to per-colour radial polynomial definitions.
> + */
> +
> +/**
> + * \class LscPolynomial
> + * \brief Radial Polynomial lsc algorithm implementation
> + *
> + * Polynomial-based lsc algorithm implementation. The LscPolynomial class
> + * implements lsc support using Polynomial to represent the shading artifacts
> + * map.
> + *
> + * \sa LscImplementation
> + * \sa LscAlgorithm
> + *
> + */
>  
> -} // namespace ipa
> -} // namespace libcamera
> +} /* namespace ipa */
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/lsc_polynomial.h b/src/ipa/libipa/lsc_polynomial.h
> index d7d9ae42e360..72cca591f260 100644
> --- a/src/ipa/libipa/lsc_polynomial.h
> +++ b/src/ipa/libipa/lsc_polynomial.h
> @@ -1,15 +1,17 @@
>  /* SPDX-License-Identifier: LGPL-2.1-or-later */
>  /*
> - * Copyright (C) 2024, Ideas On Board
> + * Copyright (C) 2026, Ideas On Board
>   *
> - * Helper for radial polynomial used in lens shading correction.
> + * Polynomial based lens shading correction
>   */
> +
>  #pragma once
>  
>  #include <algorithm>
>  #include <array>
> -#include <assert.h>
>  #include <cmath>
> +#include <optional>
> +#include <tuple>
>  
>  #include <libcamera/base/log.h>
>  #include <libcamera/base/span.h>
> @@ -18,64 +20,117 @@
>  
>  #include "libcamera/internal/value_node.h"
>  
> +#include "lsc_base.h"
> +
>  namespace libcamera {
>  
>  LOG_DECLARE_CATEGORY(LscPolynomial)
>  
>  namespace ipa {
>  
> -class LscPolynomial
> +class Polynomial

If we rename this class it should be moved into the ipa::lsc::
namespace as it is not a generic polynomial class.

>  {
>  public:
> -       LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> -                     double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> -                     double k4 = 0.0)
> +       Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> +                  double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> +                  double k4 = 0.0)
>                 : cx_(cx), cy_(cy), cnx_(0), cny_(0),
>                   coefficients_({ k0, k1, k2, k3, k4 })
>         {
>         }
>  
> -       double sampleAtNormalizedPixelPos(double x, double y) const
> +       Polynomial(const Polynomial &other) = default;
> +
> +       double sampleAtNormalizedPixelPos(double x, double y) const;
> +       double getM() const;
> +       void setReferenceImageSize(const Size &size);
> +
> +private:
> +       double cx_;
> +       double cy_;
> +       double cnx_;
> +       double cny_;
> +       std::array<double, 5> coefficients_;
> +       Size imageSize_;
> +};
> +
> +class LscPolynomialBase
> +{
> +private:
> +       using Components = std::map<std::string, Polynomial>;

> +       using ComponentsMap = std::map<unsigned int, Components>;
> +
> +public:
> +       int parseLscData(const ValueNode &yamlSets,
> +                        const LscDescriptor &descriptor);
> +
> +protected:
> +       ComponentsMap lscData_;
> +};
> +
> +template<typename T, typename U>
> +class LscPolynomial : public LscPolynomialBase, public LscImplementation<T, U>
> +{
> +public:
> +       ~LscPolynomial() {}
> +
> +       int parseLscData(const ValueNode &yamlSets,
> +                        const LscDescriptor &descriptor) override
>         {
> -               double dx = x - cnx_;
> -               double dy = y - cny_;
> -               double r = sqrt(dx * dx + dy * dy);
> -               double res = 1.0;
> -               for (unsigned int i = 0; i < coefficients_.size(); i++) {
> -                       res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> -               }
> -               return res;
> +               return LscPolynomialBase::parseLscData(yamlSets, descriptor);
>         }
>  
> -       double getM() const
> +       lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> +                                             const std::vector<double> &xPos,
> +                                             const std::vector<double> &yPos) override
>         {
> -               double cpx = imageSize_.width * cx_;
> -               double cpy = imageSize_.height * cy_;
> -               double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
> -               double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
> +               lsc::ComponentsMap<T> components;
>  
> -               return sqrt(mx * mx + my * my);
> -       }
> +               for (auto &[t, c] : lscData_) {
> +                       lsc::Components<T> comp;
>  
> -       void setReferenceImageSize(const Size &size)
> -       {
> -               assert(!size.isNull());
> -               imageSize_ = size;
> +                       for (auto &[k, p] : c) {
> +                               comp.emplace(std::piecewise_construct,
> +                                            std::forward_as_tuple(k),
> +                                            std::forward_as_tuple(samplePolynomial(p, xPos, yPos,
> +                                                                                   cropRectangle)));
> +                       }
>  
> -               /* Calculate normalized centers */
> -               double m = getM();
> -               cnx_ = (size.width * cx_) / m;
> -               cny_ = (size.height * cy_) / m;
> +                       components[t] = comp;
> +               }
> +
> +               return components;
>         }
>  
>  private:
> -       double cx_;
> -       double cy_;
> -       double cnx_;
> -       double cny_;
> -       std::array<double, 5> coefficients_;
> +       std::vector<T> samplePolynomial(const Polynomial &poly,
> +                                       Span<const double> xPositions,
> +                                       Span<const double> yPositions,
> +                                       const Rectangle &cropRectangle)
>  
> -       Size imageSize_;
> +       {
> +               double m = poly.getM();
> +               double x0 = cropRectangle.x / m;
> +               double y0 = cropRectangle.y / m;
> +               double w = cropRectangle.width / m;
> +               double h = cropRectangle.height / m;
> +               std::vector<T> samples;
> +
> +               samples.reserve(xPositions.size() * yPositions.size());
> +
> +               for (double y : yPositions) {
> +                       for (double x : xPositions) {
> +                               double xp = x0 + x * w;
> +                               double yp = y0 + y * h;
> +                               float sample = static_cast<float>
> +                                               (poly.sampleAtNormalizedPixelPos(xp, yp));
> +
> +                               samples.push_back(U(sample).quantized());
> +                       }
> +               }
> +
> +               return samples;
> +       }
>  };
>  
>  } /* namespace ipa */
> @@ -83,8 +138,8 @@ private:
>  #ifndef __DOXYGEN__
>  
>  template<>
> -struct ValueNode::Accessor<ipa::LscPolynomial> {
> -       std::optional<ipa::LscPolynomial> get(const ValueNode &obj) const
> +struct ValueNode::Accessor<ipa::Polynomial> {
> +       std::optional<ipa::Polynomial> get(const ValueNode &obj) const
>         {
>                 std::optional<double> cx = obj["cx"].get<double>();
>                 std::optional<double> cy = obj["cy"].get<double>();
> @@ -98,7 +153,7 @@ struct ValueNode::Accessor<ipa::LscPolynomial> {
>                         LOG(LscPolynomial, Error)
>                                 << "Polynomial is missing a parameter";
>  
> -               return ipa::LscPolynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);
> +               return ipa::Polynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);
>         }
>  };
>  
> diff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build
> index edf8eabd8b78..da6ea0c5e130 100644
> --- a/src/ipa/libipa/meson.build
> +++ b/src/ipa/libipa/meson.build
> @@ -14,6 +14,9 @@ libipa_headers = files([
>      'fixedpoint.h',
>      'histogram.h',
>      'interpolator.h',
> +    'lsc.h',
> +    'lsc_base.h',
> +    'lsc_grid.h',
>      'lsc_polynomial.h',
>      'lux.h',
>      'module.h',
> @@ -36,6 +39,8 @@ libipa_sources = files([
>      'fixedpoint.cpp',
>      'histogram.cpp',
>      'interpolator.cpp',
> +    'lsc.cpp',
> +    'lsc_grid.cpp',
>      'lsc_polynomial.cpp',
>      'lux.cpp',
>      'module.cpp',
> diff --git a/src/ipa/rkisp1/algorithms/lsc.cpp b/src/ipa/rkisp1/algorithms/lsc.cpp
> index 25f7c512b35c..51076737ffa5 100644
> --- a/src/ipa/rkisp1/algorithms/lsc.cpp
> +++ b/src/ipa/rkisp1/algorithms/lsc.cpp
> @@ -2,7 +2,7 @@
>  /*
>   * Copyright (C) 2021-2022, Ideas On Board
>   *
> - * RkISP1 Lens Shading Correction control
> + * RkISP1 Lens Shading Correction algorithm
>   */
>  
>  #include "lsc.h"
> @@ -14,56 +14,17 @@
>  #include <libcamera/base/log.h>
>  #include <libcamera/base/utils.h>
>  
> -#include "libcamera/internal/value_node.h"
> -
> -#include "libipa/lsc_polynomial.h"
> -#include "linux/rkisp1-config.h"
> -
>  /**
>   * \file lsc.h
>   */
>  
>  namespace libcamera {
>  
> -namespace ipa {
> -
> -template<typename T>
> -void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,
> -                      std::vector<T> &dest, double lambda)
> -{
> -       ASSERT(a.size() == b.size());
> -       dest.resize(a.size());
> -       for (size_t i = 0; i < a.size(); i++)
> -               dest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;
> -}
> -
> -template<>
> -void Interpolator<rkisp1::algorithms::LensShadingCorrection::Components>::
> -       interpolate(const rkisp1::algorithms::LensShadingCorrection::Components &a,
> -                   const rkisp1::algorithms::LensShadingCorrection::Components &b,
> -                   rkisp1::algorithms::LensShadingCorrection::Components &dest,
> -                   double lambda)
> -{
> -       interpolateVector(a.r, b.r, dest.r, lambda);
> -       interpolateVector(a.gr, b.gr, dest.gr, lambda);
> -       interpolateVector(a.gb, b.gb, dest.gb, lambda);
> -       interpolateVector(a.b, b.b, dest.b, lambda);
> -}
> -
> -} /* namespace ipa */
> -
>  namespace ipa::rkisp1::algorithms {
>  
>  /**
>   * \class LensShadingCorrection
>   * \brief RkISP1 Lens Shading Correction control
> - *
> - * Due to the optical characteristics of the lens, the light intensity received
> - * by the sensor is not uniform.
> - *
> - * The Lens Shading Correction algorithm applies multipliers to all pixels
> - * to compensate for the lens shading effect. The coefficients are
> - * specified in a downscaled table in the YAML tuning file.
>   */
>  
>  LOG_DEFINE_CATEGORY(RkISP1Lsc)
> @@ -72,265 +33,20 @@ namespace {
>  
>  constexpr int kColourTemperatureQuantization = 10;
>  
> -class LscPolynomialShadingDescriptor : public LensShadingCorrection::ShadingDescriptor
> -{
> -public:
> -       LscPolynomialShadingDescriptor(const LscPolynomial &pr, const LscPolynomial &pgr,
> -                                      const LscPolynomial &pgb, const LscPolynomial &pb)
> -               : pr_(pr), pgr_(pgr), pgb_(pgb), pb_(pb)
> -       {
> -       }
> -
> -       LensShadingCorrection::Components sampleForCrop(const Rectangle &cropRectangle,
> -                                                       Span<const double> xSizes,
> -                                                       Span<const double> ySizes) override;
> -
> -private:
> -       std::vector<uint16_t> samplePolynomial(const LscPolynomial &poly,
> -                                              Span<const double> xPositions,
> -                                              Span<const double> yPositions,
> -                                              const Rectangle &cropRectangle);
> -
> -       std::vector<double> sizesListToPositions(Span<const double> sizes);
> -
> -       LscPolynomial pr_;
> -       LscPolynomial pgr_;
> -       LscPolynomial pgb_;
> -       LscPolynomial pb_;
> -};
> -
> -LensShadingCorrection::Components
> -LscPolynomialShadingDescriptor::sampleForCrop(const Rectangle &cropRectangle,
> -                                             Span<const double> xSizes,
> -                                             Span<const double> ySizes)
> -{
> -       std::vector<double> xPos = sizesListToPositions(xSizes);
> -       std::vector<double> yPos = sizesListToPositions(ySizes);
> -
> -       return {
> -               .r = samplePolynomial(pr_, xPos, yPos, cropRectangle),
> -               .gr = samplePolynomial(pgr_, xPos, yPos, cropRectangle),
> -               .gb = samplePolynomial(pgb_, xPos, yPos, cropRectangle),
> -               .b = samplePolynomial(pb_, xPos, yPos, cropRectangle)
> -       };
> -}
> -
> -std::vector<uint16_t>
> -LscPolynomialShadingDescriptor::samplePolynomial(const LscPolynomial &poly,
> -                                                Span<const double> xPositions,
> -                                                Span<const double> yPositions,
> -                                                const Rectangle &cropRectangle)
> -{
> -       double m = poly.getM();
> -       double x0 = cropRectangle.x / m;
> -       double y0 = cropRectangle.y / m;
> -       double w = cropRectangle.width / m;
> -       double h = cropRectangle.height / m;
> -       std::vector<uint16_t> samples;
> -
> -       samples.reserve(xPositions.size() * yPositions.size());
> -
> -       for (double y : yPositions) {
> -               for (double x : xPositions) {
> -                       double xp = x0 + x * w;
> -                       double yp = y0 + y * h;
> -                       /*
> -                        * The hardware uses 2.10 fixed point format and limits
> -                        * the legal values to [1..3.999]. Scale and clamp the
> -                        * sampled value accordingly.
> -                        */
> -                       int v = static_cast<int>(
> -                               poly.sampleAtNormalizedPixelPos(xp, yp) *
> -                               1024);
> -                       v = std::clamp(v, 1024, 4095);
> -                       samples.push_back(v);
> -               }
> -       }
> -       return samples;
> -}
> -
> -/*
> - * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left
> - * quadrant of the grid. This is then mirrored in hardware to the other
> - * quadrants. See parseSizes() for further details. For easier handling, this
> - * function converts the cell sizes of half the grid to a list of position of
> - * the whole grid (on one axis). Example:
> - *
> - * input:   | 0.2 | 0.3 |
> - * output: 0.0   0.2   0.5   0.8   1.0
> - */
> -std::vector<double>
> -LscPolynomialShadingDescriptor::sizesListToPositions(Span<const double> sizes)
> -{
> -       const int half = sizes.size();
> -       std::vector<double> positions(half * 2 + 1);
> -       double x = 0.0;
> -
> -       positions[half] = 0.5;
> -       for (int i = 1; i <= half; i++) {
> -               x += sizes[half - i];
> -               positions[half - i] = 0.5 - x;
> -               positions[half + i] = 0.5 + x;
> -       }
> -
> -       return positions;
> -}
> -
> -class LscPolynomialLoader
> -{
> -public:
> -       LscPolynomialLoader(const Size &sensorSize)
> -               : sensorSize_(sensorSize)
> -       {
> -       }
> -
> -       int parseLscData(const ValueNode &yamlSets,
> -                        LensShadingCorrection::ShadingDescriptorMap &lscData);
> -
> -private:
> -       Size sensorSize_;
> -};
> -
> -int LscPolynomialLoader::parseLscData(const ValueNode &yamlSets,
> -                                     LensShadingCorrection::ShadingDescriptorMap &lscData)
> +unsigned int quantize(unsigned int value, unsigned int step)
>  {
> -       const auto &sets = yamlSets.asList();
> -       for (const auto &yamlSet : sets) {
> -               std::optional<LscPolynomial> pr, pgr, pgb, pb;
> -               uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> -
> -               if (lscData.count(ct)) {
> -                       LOG(RkISP1Lsc, Error)
> -                               << "Multiple sets found for "
> -                               << "color temperature " << ct;
> -                       return -EINVAL;
> -               }
> -
> -               pr = yamlSet["r"].get<LscPolynomial>();
> -               pgr = yamlSet["gr"].get<LscPolynomial>();
> -               pgb = yamlSet["gb"].get<LscPolynomial>();
> -               pb = yamlSet["b"].get<LscPolynomial>();
> -
> -               if (!(pr || pgr || pgb || pb)) {
> -                       LOG(RkISP1Lsc, Error)
> -                               << "Failed to parse polynomial for "
> -                               << "colour temperature " << ct;
> -                       return -EINVAL;
> -               }
> -
> -               pr->setReferenceImageSize(sensorSize_);
> -               pgr->setReferenceImageSize(sensorSize_);
> -               pgb->setReferenceImageSize(sensorSize_);
> -               pb->setReferenceImageSize(sensorSize_);
> -
> -               lscData.emplace(
> -                       ct, std::make_unique<LscPolynomialShadingDescriptor>(
> -                                   *pr, *pgr, *pgb, *pb));
> -       }
> -
> -       if (lscData.empty()) {
> -               LOG(RkISP1Lsc, Error) << "Failed to load any sets";
> -               return -EINVAL;
> -       }
> -
> -       return 0;
> +       return std::lround(value / static_cast<double>(step)) * step;
>  }
>  
> -class LscTableShadingDescriptor : public LensShadingCorrection::ShadingDescriptor
> -{
> -public:
> -       LscTableShadingDescriptor(LensShadingCorrection::Components components)
> -               : lscData_(std::move(components))
> -       {
> -       }
> -
> -       LensShadingCorrection::Components
> -       sampleForCrop([[maybe_unused]] const Rectangle &cropRectangle,
> -                     [[maybe_unused]] Span<const double> xSizes,
> -                     [[maybe_unused]] Span<const double> ySizes) override
> -       {
> -               LOG(RkISP1Lsc, Warning)
> -                       << "Tabular LSC data doesn't support resampling";
> -               return lscData_;
> -       }
> -
> -private:
> -       LensShadingCorrection::Components lscData_;
> -};
> -
> -class LscTableLoader
> -{
> -public:
> -       int parseLscData(const ValueNode &yamlSets,
> -                        LensShadingCorrection::ShadingDescriptorMap &lscData);
> -
> -private:
> -       std::vector<uint16_t> parseTable(const ValueNode &tuningData,
> -                                        const char *prop);
> -};
> -
> -int LscTableLoader::parseLscData(const ValueNode &yamlSets,
> -                                LensShadingCorrection::ShadingDescriptorMap &lscData)
> -{
> -       const auto &sets = yamlSets.asList();
> -
> -       for (const auto &yamlSet : sets) {
> -               uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> -
> -               if (lscData.count(ct)) {
> -                       LOG(RkISP1Lsc, Error)
> -                               << "Multiple sets found for color temperature "
> -                               << ct;
> -                       return -EINVAL;
> -               }
> -
> -               LensShadingCorrection::Components set;
> -               set.r = parseTable(yamlSet, "r");
> -               set.gr = parseTable(yamlSet, "gr");
> -               set.gb = parseTable(yamlSet, "gb");
> -               set.b = parseTable(yamlSet, "b");
> -
> -               if (set.r.empty() || set.gr.empty() ||
> -                   set.gb.empty() || set.b.empty()) {
> -                       LOG(RkISP1Lsc, Error)
> -                               << "Set for color temperature " << ct
> -                               << " is missing tables";
> -                       return -EINVAL;
> -               }
> -
> -               lscData.emplace(
> -                       ct, std::make_unique<LscTableShadingDescriptor>(std::move(set)));
> -       }
> -
> -       if (lscData.empty()) {
> -               LOG(RkISP1Lsc, Error) << "Failed to load any sets";
> -               return -EINVAL;
> -       }
> -
> -       return 0;
> -}
> +} /* namespace */
>  
> -std::vector<uint16_t> LscTableLoader::parseTable(const ValueNode &tuningData,
> -                                                const char *prop)
> +LensShadingCorrection::LensShadingCorrection()
> +       : lastAppliedCt_(0), lastAppliedQuantizedCt_(0)
>  {
> -       static constexpr unsigned int kLscNumSamples =
> -               RKISP1_CIF_ISP_LSC_SAMPLES_MAX * RKISP1_CIF_ISP_LSC_SAMPLES_MAX;
> -
> -       std::vector<uint16_t> table =
> -               tuningData[prop].get<std::vector<uint16_t>>().value_or(utils::defopt);
> -       if (table.size() != kLscNumSamples) {
> -               LOG(RkISP1Lsc, Error)
> -                       << "Invalid '" << prop << "' values: expected "
> -                       << kLscNumSamples
> -                       << " elements, got " << table.size();
> -               return {};
> -       }
> -
> -       return table;
>  }
>  
> -std::vector<double> parseSizes(const ValueNode &tuningData,
> -                              const char *prop)
> +std::vector<double> LensShadingCorrection::parseSizes(const ValueNode &tuningData,
> +                                                     const char *prop)
>  {
>         std::vector<double> sizes =
>                 tuningData[prop].get<std::vector<double>>().value_or(utils::defopt);
> @@ -359,22 +75,36 @@ std::vector<double> parseSizes(const ValueNode &tuningData,
>         return sizes;
>  }
>  
> -unsigned int quantize(unsigned int value, unsigned int step)
> +/*
> + * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left
> + * quadrant of the grid. This is then mirrored in hardware to the other
> + * quadrants. See parseSizes() for further details. For easier handling, this
> + * function converts the cell sizes of half the grid to a list of position of
> + * the whole grid (on one axis). Example:
> + *
> + * input:   | 0.2 | 0.3 |
> + * output: 0.0   0.2   0.5   0.8   1.0
> + */
> +std::vector<double> LensShadingCorrection::sizesToPositions(Span<const double> sizes)
>  {
> -       return std::lround(value / static_cast<double>(step)) * step;
> -}
> +       const int half = sizes.size();
> +       std::vector<double> positions(half * 2 + 1);
> +       double x = 0.0;
>  
> -} /* namespace */
> +       positions[half] = 0.5;
> +       for (int i = 1; i <= half; i++) {
> +               x += sizes[half - i];
> +               positions[half - i] = 0.5 - x;
> +               positions[half + i] = 0.5 + x;
> +       }
>  
> -LensShadingCorrection::LensShadingCorrection()
> -       : lastAppliedCt_(0), lastAppliedQuantizedCt_(0)
> -{
> +       return positions;
>  }
>  
>  /**
>   * \copydoc libcamera::ipa::Algorithm::init
>   */
> -int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
> +int LensShadingCorrection::init(IPAContext &context,
>                                 const ValueNode &tuningData)
>  {
>         xSize_ = parseSizes(tuningData, "x-size");
> @@ -383,56 +113,27 @@ int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
>         if (xSize_.empty() || ySize_.empty())
>                 return -EINVAL;
>  
> -       /* Get all defined sets to apply. */
> -       const ValueNode &yamlSets = tuningData["sets"];
> -       if (!yamlSets.isList()) {
> -               LOG(RkISP1Lsc, Error)
> -                       << "'sets' parameter not found in tuning file";
> -               return -EINVAL;
> -       }
> -
> -       ShadingDescriptorMap lscData;
> -       int ret = 0;
> -
> -       std::string type = tuningData["type"].get<std::string>("table");
> -       if (type == "table") {
> -               LOG(RkISP1Lsc, Debug) << "Loading tabular LSC data.";
> -               auto loader = LscTableLoader();
> -               ret = loader.parseLscData(yamlSets, lscData);
> -       } else if (type == "polynomial") {
> -               LOG(RkISP1Lsc, Debug) << "Loading polynomial LSC data.";
> -               /*
> -                * \todo: Most likely the reference frame should be native_size.
> -                * Let's wait how the internal discussions progress.
> -                */
> -               auto loader = LscPolynomialLoader(context.sensorInfo.activeAreaSize);
> -               ret = loader.parseLscData(yamlSets, lscData);
> -       } else {
> -               LOG(RkISP1Lsc, Error) << "Unsupported LSC data type '"
> -                                     << type << "'";
> -               ret = -EINVAL;
> -       }
> -
> -       if (ret)
> -               return ret;
> +       xPos_ = sizesToPositions(xSize_);
> +       yPos_ = sizesToPositions(ySize_);
>  
> -       context.ctrlMap[&controls::LensShadingCorrectionEnable] =
> -               ControlInfo(false, true, true);
> -
> -       shadingDescriptors_ = std::move(lscData);
> -
> -       return 0;
> +       return lscAlgo_.init(tuningData, context.ctrlMap, {
> +                               .keys = { "r", "gr", "gb", "b" },
> +                               .numHCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,
> +                               .numVCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,
> +                               .sensorSize = context.sensorInfo.activeAreaSize
> +                            });
>  }
>  
>  /**
>   * \copydoc libcamera::ipa::Algorithm::configure
>   */
>  int LensShadingCorrection::configure(IPAContext &context,
> -                                    [[maybe_unused]] const IPACameraSensorInfo &configInfo)
> +                                    const IPACameraSensorInfo &configInfo)
>  {
>         const Size &size = context.configuration.sensor.size;
>         Size totalSize{};
>  
> +       /* Calculate gradients. */
>         for (unsigned int i = 0; i < RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE; ++i) {
>                 xSizes_[i] = xSize_[i] * size.width;
>                 ySizes_[i] = ySize_[i] * size.height;
> @@ -455,16 +156,8 @@ int LensShadingCorrection::configure(IPAContext &context,
>                 yGrad_[i] = std::round(32768 / ySizes_[i]);
>         }
>  
> -       LOG(RkISP1Lsc, Debug) << "Sample LSC data for " << configInfo.analogCrop;
> -       std::map<unsigned int, LensShadingCorrection::Components> shadingData;
> -       for (const auto &[t, descriptor] : shadingDescriptors_)
> -               shadingData[t] = descriptor->sampleForCrop(configInfo.analogCrop,
> -                                                          xSize_, ySize_);
> -
> -       sets_.setData(std::move(shadingData));
> -
> -       context.activeState.lsc.enabled = true;
> -       return 0;
> +       return lscAlgo_.configure(context.activeState.lsc, configInfo.analogCrop,
> +                                 xPos_, yPos_);
>  }
>  
>  void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)
> @@ -476,12 +169,16 @@ void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)
>  }
>  
>  void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
> -                                     const Components &set)
> +                                     const ipa::lsc::Components<uint16_t> &set)
>  {
> -       std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
> -       std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
> -       std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
> -       std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
> +       const auto &r = set.at("r");
> +       std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
> +       const auto &gr = set.at("gr");
> +       std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
> +       const auto &gb = set.at("gb");
> +       std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
> +       const auto &b = set.at("b");
> +       std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
>  }
>  
>  /**
> @@ -492,19 +189,8 @@ void LensShadingCorrection::queueRequest(IPAContext &context,
>                                          IPAFrameContext &frameContext,
>                                          const ControlList &controls)
>  {
> -       auto &lsc = context.activeState.lsc;
> -
> -       const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);
> -       if (lscEnable && *lscEnable != lsc.enabled) {
> -               lsc.enabled = *lscEnable;
> -
> -               LOG(RkISP1Lsc, Debug)
> -                       << (lsc.enabled ? "Enabling" : "Disabling") << " Lsc";
> -
> -               frameContext.lsc.update = true;
> -       }
> -
> -       frameContext.lsc.enabled = lsc.enabled;
> +       lscAlgo_.queueRequest(context.activeState.lsc, frameContext.lsc,
> +                             controls);
>  }
>  
>  /**
> @@ -542,7 +228,7 @@ void LensShadingCorrection::prepare([[maybe_unused]] IPAContext &context,
>  
>         setParameters(*config);
>  
> -       const Components &set = sets_.getInterpolated(quantizedCt);
> +       const auto &set = lscAlgo_.interpolateComponents(quantizedCt);
>         copyTable(*config, set);
>  
>         lastAppliedCt_ = ct;
> @@ -562,7 +248,7 @@ void LensShadingCorrection::process([[maybe_unused]] IPAContext &context,
>                                     [[maybe_unused]] const rkisp1_stat_buffer *stats,
>                                     ControlList &metadata)
>  {
> -       metadata.set(controls::LensShadingCorrectionEnable, frameContext.lsc.enabled);
> +       lscAlgo_.process(frameContext.lsc, metadata);
>  }
>  
>  REGISTER_IPA_ALGORITHM(LensShadingCorrection, "LensShadingCorrection")
> diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
> index 0a256e225327..7c656d95acfb 100644
> --- a/src/ipa/rkisp1/algorithms/lsc.h
> +++ b/src/ipa/rkisp1/algorithms/lsc.h
> @@ -2,17 +2,23 @@
>  /*
>   * Copyright (C) 2021-2022, Ideas On Board
>   *
> - * RkISP1 Lens Shading Correction control
> + * RkISP1 Lens Shading Correction algorithm
>   */
>  
>  #pragma once
>  
> -#include <map>
> -#include <memory>
> +#include <vector>
>  
> -#include "libipa/interpolator.h"
> +#include <linux/rkisp1-config.h>
> +
> +#include "libcamera/internal/value_node.h"
> +
> +#include "libipa/fixedpoint.h"
> +#include "libipa/lsc.h"
>  
>  #include "algorithm.h"
> +#include "ipa_context.h"
> +#include "params.h"
>  
>  namespace libcamera {
>  
> @@ -37,39 +43,28 @@ public:
>                      const rkisp1_stat_buffer *stats,
>                      ControlList &metadata) override;
>  
> -       struct Components {
> -               std::vector<uint16_t> r;
> -               std::vector<uint16_t> gr;
> -               std::vector<uint16_t> gb;
> -               std::vector<uint16_t> b;
> -       };
> -
> -       class ShadingDescriptor
> -       {
> -       public:
> -               virtual ~ShadingDescriptor() = default;
> -               virtual Components sampleForCrop(const Rectangle &cropRectangle,
> -                                                Span<const double> xSizes,
> -                                                Span<const double> ySizes) = 0;
> -       };
> -
> -       using ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;
> -
>  private:
> +       std::vector<double> parseSizes(const ValueNode &tuningData,
> +                                      const char *prop);
> +       std::vector<double> sizesToPositions(Span<const double> sizes);
> +
>         void setParameters(rkisp1_cif_isp_lsc_config &config);
> -       void copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);
> +       void copyTable(rkisp1_cif_isp_lsc_config &config,
> +                      const ipa::lsc::Components<uint16_t> &set0);
>  
> -       ShadingDescriptorMap shadingDescriptors_;
> -       ipa::Interpolator<Components> sets_;
>         std::vector<double> xSize_;
>         std::vector<double> ySize_;
>         uint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
>         uint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
>         uint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
>         uint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> +       std::vector<double> xPos_;
> +       std::vector<double> yPos_;
>  
>         unsigned int lastAppliedCt_;
>         unsigned int lastAppliedQuantizedCt_;
> +
> +       LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;

I don't really like specifying the fixed format at compile time, as we
will have a hard time to implement support for different formats at
runtime like Mali supports. But I also don't see an easy way out of
that.

>  };
>  
>  } /* namespace ipa::rkisp1::algorithms */
> diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
> index cd97e10bcf2b..005f4102b4f6 100644
> --- a/src/ipa/rkisp1/ipa_context.h
> +++ b/src/ipa/rkisp1/ipa_context.h
> @@ -30,6 +30,7 @@
>  #include "libipa/ccm.h"
>  #include "libipa/fc_queue.h"
>  #include "libipa/fixedpoint.h"
> +#include "libipa/lsc.h"
>  
>  namespace libcamera {
>  
> @@ -138,9 +139,7 @@ struct IPAActiveState {
>                 double strength;
>         } wdr;
>  
> -       struct {
> -               bool enabled;
> -       } lsc;
> +       ipa::lsc::ActiveState lsc;

That will really ease IPA development.

>  };
>  
>  struct IPAFrameContext : public FrameContext {
> @@ -213,10 +212,7 @@ struct IPAFrameContext : public FrameContext {
>                 double gain;
>         } wdr;
>  
> -       struct {
> -               bool enabled;
> -               bool update;
> -       } lsc;
> +       ipa::lsc::FrameContext lsc;
>  };

This is a really massive patch and I wonder if it would be possible to
split it out into multiple changes (also in case we have regressions). I
know this is more work, but otoh only rkisp1 uses that. So it would be
possible?

Best regards,
Stefan

>  
>  struct IPAContext {
> 
> -- 
> 2.54.0
>
Jacopo Mondi June 26, 2026, 2:26 p.m. UTC | #6
Hi Stefan

On Fri, Jun 26, 2026 at 11:18:12AM +0200, Stefan Klug wrote:
> Hi Jacopo,
>
> Thank you for the refactoring.
>
> Quoting Jacopo Mondi (2026-06-15 16:05:34)
> > Introduce an Lsc algorithm implementation in libIPA.
> >
> > The LscAlgorithm class supports two algorithms: grid-based lsc and
> > polynomial-based lsc.
> >
> > Both implementations have been generalized from the existing ones in the
> > RkISP1 IPA.
> >
> > Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> > ---
> >  src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++
> >  src/ipa/libipa/lsc.h              | 145 +++++++++++++
> >  src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++
> >  src/ipa/libipa/lsc_base.h         |  59 ++++++
> >  src/ipa/libipa/lsc_grid.cpp       |  33 +++
> >  src/ipa/libipa/lsc_grid.h         | 113 ++++++++++
> >  src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-
> >  src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----
> >  src/ipa/libipa/meson.build        |   5 +
> >  src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------
> >  src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--
> >  src/ipa/rkisp1/ipa_context.h      |  10 +-
> >  12 files changed, 1126 insertions(+), 450 deletions(-)
> >
> > diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
> > new file mode 100644
> > index 000000000000..ae713a776778
> > --- /dev/null
> > +++ b/src/ipa/libipa/lsc.cpp
> > @@ -0,0 +1,322 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2026 Ideas on Board Oy
> > + *
> > + * libIPA Lsc algorithms
> > + */
> > +
> > +#include "lsc.h"
> > +
> > +#include <libcamera/base/log.h>
> > +
> > +/**
> > + * \file lsc.h
> > + * \brief libipa lsc algorithm
> > + */
> > +
> > +namespace libcamera {
> > +
> > +LOG_DEFINE_CATEGORY(Lsc)
> > +
> > +namespace ipa {
> > +
> > +namespace lsc {
> > +
> > +/**
> > + * \struct ActiveState
> > + * \brief The lsc active state
> > + *
> > + * \var ActiveState::enabled
> > + * \brief Boolean flag for the LscAlgorithm enable status
> > + */
> > +
> > +/**
> > + * \struct FrameContext
> > + * \brief The lsc frame context
> > + *
> > + * \var FrameContext::enabled
> > + * \brief Boolean flag for the LscAlgorithm enable status
> > + *
> > + * \var FrameContext::update
> > + * \brief Boolean flag for the LscAlgorithm updated status
> > + */
> > +
> > +} /* namespace lsc */
> > +
> > +#ifndef __DOXYGEN__
> > +template<>
> > +void Interpolator<lsc::Components<uint16_t>>::
> > +       interpolate(const lsc::Components<uint16_t> &a,
> > +                   const lsc::Components<uint16_t> &b,
> > +                   lsc::Components<uint16_t> &dest,
> > +                   double lambda)
> > +{
> > +       for (auto const &[k, v] : a)
> > +               interpolateVector(v, b.at(k), dest[k], lambda);
> > +}
> > +#endif
> > +
> > +/**
> > + * \class LscAlgorithmBase
> > + * \brief Base class for LscAlgorithm
> > + *
> > + * Base class for LscAlgorithm for non-templated functions implementation
> > + */
> > +
> > +/**
> > + * \brief Queue a request to the lsc algorithm
> > + * \param[in] state The lsc active state
> > + * \param[in] context The lsc frame context
> > + * \param[in] controls The list of controls associated with a Request
> > + *
> > + * Queue a new list of \a controls to the lsc algorithm.
> > + * The only supported control is controls::LensShadingCorrectionEnable.
> > + */
> > +void LscAlgorithmBase::queueRequest(lsc::ActiveState &state,
> > +                                   lsc::FrameContext &context,
> > +                                   const ControlList &controls)
> > +{
> > +       const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);
> > +       if (lscEnable && *lscEnable != state.enabled) {
> > +               state.enabled = *lscEnable;
> > +
> > +               LOG(Lsc, Debug)
> > +                       << (state.enabled ? "Enabling" : "Disabling") << " Lsc";
> > +
> > +               context.update = true;
> > +       }
> > +
> > +       context.enabled = state.enabled;
> > +}
> > +
> > +/**
> > + * \brief Populate the list of lsc metadata
> > + * \param[in] context The lsc frame context
> > + * \param[in] metadata The list of metadata
> > + *
> > + * Populates the list of \a metadata with controls handled by the LscAlgorithm
> > + * class. The only supported metadata is controls::LensShadingCorrectionEnable.
> > + */
> > +void LscAlgorithmBase::process(lsc::FrameContext &context, ControlList &metadata)
> > +{
> > +       metadata.set(controls::LensShadingCorrectionEnable, context.enabled);
> > +}
> > +
> > +/**
> > + * \class LscAlgorithm
> > + * \brief libIPA lsc algorithm implementation
> > + * \tparam T The type used to store the gain values when loaded from tuning file
> > + * \tparam U The fixedpoint lsc engine register format
> > + *
> > + * Due to the optical characteristics of the lens, the light intensity received
> > + * by the sensor is not uniform. The Lens Shading Correction algorithm applies
> > + * multipliers to all pixels to compensate for the lens shading effect.
> > + *
> > + * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.
> > + * It provides support for parsing the tuning file content and generate tables
> > + * of per-colour temperature gains that IPA algorithms can use to program their
> > + * lsc engine.
> > + *
> > + * The init() function parses the tuning file and loads the gain tables either
> > + * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain
> > + * tables are organized per-colour temperature with per-colour components gain
> > + * vectors or polynomial coefficients. The colour components names are
> > + * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc
> > + * engine support 4 colour components (r, gr, gb, b), some only support 3 colour
> > + * components (r, g, b). The name (and number) of the expected colour components
> > + * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys
> > + * field.
> > + *
> > + * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour
> > + * components. The gain table has been here omitted, but the expected number
>
> Nit: s/here omitted/omitted here/
>

I think 'here omitted' is correct

> > + * of entries has be equal to
>
> Nit: has to be
>
> > + * LscDescriptor::numHCells * LscDescriptor::numVCells.
> > + *
> > + * \code{.yaml}
> > + * - Lsc:
> > + *    sets:
> > + *      - ct: 2500
> > + *        r: [
> > + *             .. gains table omitted..
> > + *        ]
> > + *        g: [
> > + *             .. gains table omitted..
> > + *        ]
> > + *        b: [
> > + *             .. gains table omitted..
> > + *        ]
> > + *      - ct: 6500
> > + *        r: [
> > + *             .. gains table omitted..
> > + *        ]
> > + *        g: [
> > + *             .. gains table omitted..
> > + *        ]
> > + *        b: [
> > + *             .. gains table omitted..
> > + *        ]
> > + * \endcode
> > + *
> > + * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'
> > + * colour components:
> > + *
> > + * \code{.yaml}
> > + * - Lsc:
> > + *    type: "polynomial"
> > + *    sets:
> > + *      - ct: 2500
> > + *        r:
> > + *          cx: 0.5006571711950275
> > + *          cy: 0.510093737499277
> > + *          k0: 1.5393282208428813
> > + *          k1: -1.1434559757908016
> > + *          k2: 4.332602305814554
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + *        gr:
> > + *          cx: 0.5009320529087338
> > + *          cy: 0.511208038949085
> > + *          k0: 1.5634738574805407
> > + *          k1: -1.5623484259968348
> > + *          k2: 4.846686073656501
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + *        gb:
> > + *          cx: 0.5012013290343839
> > + *          cy: 0.5128251541578288
> > + *          k0: 1.526147944919103
> > + *          k1: -1.4316976083689723
> > + *          k2: 4.792604063222728
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + *        b:
> > + *          cx: 0.49864139511067784
> > + *          cy: 0.5162095081739346
> > + *          k0: 1.0405245474038738
> > + *          k1: 0.05618339879447103
> > + *          k2: 1.8792813594001752
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + *      - ct: 6000
> > + *        r:
> > + *          cx: 0.5006202239353942
> > + *          cy: 0.5099531318307661
> > + *          k0: 1.4702946023945032
> > + *          k1: -0.8893767547927631
> > + *          k2: 3.920547732201387
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + *        gr:
> > + *          cx: 0.500907874178317
> > + *          cy: 0.511084916024106
> > + *          k0: 1.5336172760559457
> > + *          k1: -1.39964026514435
> > + *          k2: 4.565487728954618
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + *        gb:
> > + *          cx: 0.5011898608900477
> > + *          cy: 0.5126797906745105
> > + *          k0: 1.5013145790354843
> > + *          k1: -1.2747407173754124
> > + *          k2: 4.514682876897286
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + *        b:
> > + *          cx: 0.4987561413116136
> > + *          cy: 0.5159619420778772
> > + *          k0: 1.0102986422191802
> > + *          k1: 0.13263449763985727
> > + *          k2: 1.686556107316064
> > + *          k3: 0.0
> > + *          k4: 0.0
> > + * \endcode
> > + *
> > + * The lsc tables or the polynomial definition are generated at tuning time
> > + * using an image of known resolution which needs to be specified in
> > + * LscDescriptor::sensorSize.
> > + *
> > + * At LscAlgorithm::configure() time the lsc tables are re-sampled on the
> > + * sensor's crop rectangle in use to adapt them to the configuration in use for
> > + * a streaming session. Polynomial lsc tables support re-sampling and can be
> > + * applied to any sensor configuration. Grid-based lsc tables cannot be
> > + * re-sampled and the configuration as parsed from the tuning file is used for
> > + * all sensor configurations providing best-effort results.
> > + *
> > + * When the IPA algorithms wants to get access to the (resampled) tables to
> > + * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get
> > + * an lsc table interpolated by the LscAlgorithm class for the specified colour
> > + * temperature. If the algorithm wants to access the non-interpolated tables it
> > + * can retrieve them using LscAlgorithm::getComponents().
> > + */
> > +
> > +/**
> > + * \fn LscAlgorithm::init()
> > + * \param[in] tuningData The tuning data
> > + * \param[in] controls The IPA list of supported controls
> > + * \param[in] descriptor The lsc engine descriptor
> > + *
> > + * Parse \a tuningData according to the settings specified in \a descriptor to
> > + * populate the lsc data and registers lsc controls in \a controls.
> > + *
> > + * \return 0 on success, a negative error code otherwise
> > + */
> > +
> > +/**
> > + * \fn LscAlgorithm::configure()
> > + * \param[in] state The lsc active state
> > + * \param[in] analogCrop The current sensor analog crop rectangle
> > + * \param[in] xPos List of horizontal positions of the LSC grid nodes
> > + * \param[in] yPos List of vertical positions of the LSC grid nodes
> > + *
> > + * Re-sample the lsc data for an \a analogCrop.
> > + *
> > + * Lsc data are generated at tuning time using a known sensor configuration.
> > + * When a new streaming session is started, it might use a different sensor
> > + * configuration for which the lsc tables need to be adjusted to.
> > + *
> > + * This function re-generates the lsc tables to adapt them to a new sensor
> > + * configuration, specifically it re-samples the lsc data for a new \a
> > + * analogCrop on a grid specified by \a xPos and \a yPos. Re-sampling of
> > + * lsc data is only supported by polynomial-based lsc tables.
> > + *
> > + * \sa LscImplementation::resampleLscData
> > + *
> > + * \return 0 on success, a negative error code otherwise
> > + */
> > +
> > +/**
> > + * \fn LscAlgorithm::interpolateComponents
> > + * \brief Interpolate the lsc tables for a given colour temperature
> > + * \param[in] ct The colour temperature
> > + *
> > + * Lsc data are generated using different colour temperatures during the
> > + * tuning phase.
> > + *
> > + * This function returns the interpolated lsc data for a given \a ct
> > + * colour temperature.
> > + *
> > + * IPA algorithm can use this function to obtain a list of gains per-colour
> > + * component to program their lsc engines with every time a significant enough
> > + * change in colour temperature is detected.
> > + *
> > + * Calling this function is only valid after LscAlgorithm::configure() has been
> > + * called. An empty components list is returned otherwise.
> > + *
> > + * \return The lsc gains table interpolated for temperature \a ct
> > + */
> > +
> > +/**
> > + * \fn LscAlgorithm::getComponents
> > + *
> > + * Return the map of lsc data per colour temperature.
> > + *
> > + * Calling this function is only valid after LscAlgorithm::configure() has been
> > + * called. An empty components list is returned otherwise.
> > + *
> > + * \return The map of lsc gains tables per colour-temperature
> > + */
> > +
> > +} /* namespace ipa */
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/ipa/libipa/lsc.h b/src/ipa/libipa/lsc.h
> > new file mode 100644
> > index 000000000000..8bfa3a7c4ecf
> > --- /dev/null
> > +++ b/src/ipa/libipa/lsc.h
> > @@ -0,0 +1,145 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2026 Ideas on Board Oy
> > + *
> > + * libIPA Lsc algorithms
> > + */
> > +
> > +#pragma once
> > +
> > +#include <memory>
> > +
> > +#include <libcamera/control_ids.h>
> > +
> > +#include "interpolator.h"
> > +#include "lsc_grid.h"
> > +#include "lsc_polynomial.h"
> > +
> > +namespace libcamera {
> > +
> > +LOG_DECLARE_CATEGORY(Lsc)
> > +
> > +namespace ipa {
> > +
> > +namespace lsc {
> > +
> > +struct ActiveState {
> > +       bool enabled;
> > +};
> > +
> > +struct FrameContext {
> > +       bool enabled;
> > +       bool update;
> > +};
> > +
> > +} /* namespace lsc */
> > +
> > +#ifndef __DOXYGEN__
> > +template<typename T>
> > +void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,
> > +                      std::vector<T> &dest, double lambda)
> > +{
> > +       ASSERT(a.size() == b.size());
> > +       dest.resize(a.size());
> > +       for (size_t i = 0; i < a.size(); i++)
> > +               dest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;
> > +}
> > +
> > +template<>
> > +void Interpolator<lsc::Components<uint16_t>>::
> > +       interpolate(const lsc::Components<uint16_t> &a,
> > +                   const lsc::Components<uint16_t> &b,
> > +                   lsc::Components<uint16_t> &dest,
> > +                   double lambda);
> > +#endif /* __DOXYGEN__ */
> > +
> > +class LscAlgorithmBase
> > +{
> > +public:
> > +       void queueRequest(lsc::ActiveState &state, lsc::FrameContext &context,
> > +                         const ControlList &controls);
> > +       void process(lsc::FrameContext &context, ControlList &metadata);
> > +};
> > +
> > +template<typename T, typename U>
> > +class LscAlgorithm : public LscAlgorithmBase
> > +{
> > +public:
> > +       int init(const ValueNode &tuningData, ControlInfoMap::Map &controls,
> > +                LscDescriptor descriptor)
> > +       {
> > +               polynomial_ = false;
> > +
> > +               std::string type = tuningData["type"].get<std::string>("table");
> > +               if (type == "table") {
> > +                       impl_ = std::make_unique<LscGrid<T, U>>();
> > +                       LOG(Lsc, Debug) << "Using grid-based Lsc";
> > +               } else if (type == "polynomial") {
> > +                       impl_ = std::make_unique<LscPolynomial<T, U>>();
> > +                       polynomial_ = true;
> > +                       LOG(Lsc, Debug) << "Using polynomial Lsc";
> > +               } else {
> > +                       LOG(Lsc, Error) << "Unsupported Lsc algorithm '"
> > +                                       << type << "'";
> > +                       return -EINVAL;
> > +               }
> > +
> > +               const ValueNode &yamlSets = tuningData["sets"];
> > +               if (!yamlSets.isList()) {
> > +                       LOG(Lsc, Error) << "'sets' parameter not found in tuning file";
> > +                       return -EINVAL;
> > +               }
> > +
> > +               int ret = impl_->parseLscData(yamlSets, descriptor);
> > +               if (ret)
> > +                       return ret;
> > +
> > +               controls[&controls::LensShadingCorrectionEnable] =
> > +                       ControlInfo(false, true, true);
> > +
> > +               return 0;
> > +       }
> > +
> > +       int configure(lsc::ActiveState &state, const Rectangle &analogCrop,
> > +                     const std::vector<double> &xPos,
> > +                     const std::vector<double> &yPos)
> > +       {
> > +               LOG(Lsc, Debug) << "Sample Lsc data for " << analogCrop;
> > +               lsc::ComponentsMap<T> lscData =
> > +                       impl_->resampleLscData(analogCrop, xPos, yPos);
> > +
> > +               /*
> > +                * Retain a copy of the components table.
> > +                *
> > +                * We could avoid a copy here if getComponents() could
> > +                * return sets_.data() but I wasn't able to work around the
> > +                * compiler refusing it.
> > +                */
> > +               lscData_ = lscData;
> > +
> > +               sets_.setData(std::move(lscData));
> > +               state.enabled = true;
> > +
> > +               return 0;
> > +       }
> > +
> > +       const lsc::Components<T> interpolateComponents(unsigned int ct)
> > +       {
> > +               return sets_.getInterpolated(ct);
> > +       }
> > +
> > +       const lsc::ComponentsMap<T> getComponents()
> > +       {
> > +               return lscData_;
> > +       }
> > +
> > +private:
> > +       std::unique_ptr<LscImplementation<T, U>> impl_;
> > +       Interpolator<lsc::Components<T>> sets_;
> > +       lsc::ComponentsMap<T> lscData_;
> > +       bool polynomial_;
> > +};
> > +
> > +} /* namespace ipa */
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/ipa/libipa/lsc_base.cpp b/src/ipa/libipa/lsc_base.cpp
> > new file mode 100644
> > index 000000000000..fba8d2f00d54
> > --- /dev/null
> > +++ b/src/ipa/libipa/lsc_base.cpp
> > @@ -0,0 +1,152 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2026 Ideas on Board Oy
> > + *
> > + * Base classes and types for LSC algorithms implementations
> > + */
> > +
> > +#include "lsc_base.h"
> > +
> > +/**
> > + * \file lsc_base.h
> > + * \brief Base types and definitions for LscImplementation class hierarchy
> > + *
> > + * Split the common classes and types definitions to a dedicated file to avoid
> > + * circular inclusions.
> > + *
> > + * Both LscGrid and LscPolynomial inherit from LscImplementation and they are
> > + * instantiated from lsc.h as the LscAlgorithm::init() function is there inlined.
> > + *
> > + * For this reason lsc.h needs to include lsc_grid.h and lsc_polynomial.h which
> > + * need the LscImplementation class definition themselves.
> > + *
> > + * Split the LscImplementation interface definition to this file so that it can
> > + * be included by both lsc_grid.h and lsc_polynomial.h, which are then included
> > + * by lsc.h.
> > + */
> > +
> > +namespace libcamera {
> > +
> > +namespace ipa {
> > +
> > +namespace lsc {
> > +
> > +/**
> > + * \class Components
> > + * \brief Associate a colour components with a list of gains
> > + * \tparam T The type used to store the gain values when loaded from tuning file
> > + *
> > + * Lsc tables are defined as a list of gain values associated to a colour
> > + * component.
> > + *
> > + * As different ISP support different colour components (usually 'r', 'gr',
> > + * 'gb', 'b' or just 'r', 'g', 'b') this class associates a string
> > + * identifier for the colour component to a list of gains of type \a T.
> > + *
> > + * Each key name shall match an entry in the tuning file and the type \a T
> > + * shall match the size of the registers where gains are stored.
> > + *
> > + * The list of keys is provided to the LscAlgorithm class using \a
> > + * LscDescriptor::keys.
> > + */
> > +
> > +/**
> > + * \class ComponentsMap
> > + * \brief Associate a colour temperature to a lsc table
> > + * \tparam T The type used to store the gain values when loaded from tuning file
> > + *
> > + * An lsc table is generated during the tuning phase for a specific light
> > + * temperature, and a tuning file usually contains lsc tables generated for
> > + * several different colour temperatures.
> > + *
> > + * This class associates an lsc table to the colour temperature used when the
> > + * table has been generated.
> > + */
> > +
> > +} /* namespace lsc */
> > +
> > +/**
> > + * \struct LscDescriptor
> > + * \brief Describe the ISP lsc engine
> > + *
> > + * \var LscDescriptor::keys
> > + * \brief The list of colour components to which a list of gains is associated
> > + * with in the tuning file. Used for parsing the tuning file
> > + *
> > + * \var LscDescriptor::numHCells
> > + * \brief Number of horizontal cells of the ISP lsc grid. Used for validating
> > + * the list of gains parsed from tuning file
> > + *
> > + * \var LscDescriptor::numVCells
> > + * \brief Number of vertical cells of the ISP lsc grid. Used for validating
> > + * the list of gains parsed from tuning file
> > + *
> > + * \var LscDescriptor::sensorSize
> > + * \brief The physical sensor size. This is the largest frame size used to
> > + * generate the lsc table. Only used by the polynomial lsc algorithm
> > + */
> > +
> > +/**
> > + * \class LscImplementation
> > + * \brief Pure virtual base class for lsc algorithm implementations
> > + * \tparam T The type used to store the gain values when loaded from tuning file
> > + * \tparam U The fixedpoint format used to convert gain values generated by
> > + * polynomial expansion to the register format
> > + *
> > + * Defines the interface for the lsc algorithm implementation. Currently
> > + * implemented by LscGrid and LscPolynomial.
> > + */
> > +
> > +/**
> > + * \fn LscImplementation::~LscImplementation
> > + * \brief Virtual class destructor
> > + */
> > +
> > +/**
> > + * \fn LscImplementation::parseLscData
> > + * \brief Parse \a tuningData using \a descriptor
> > + * \param[in] tuningData The tuning data
> > + * \param[in] descriptor The lsc engine descriptor
> > + *
> > + * Parse the tuning file using the \a descriptor to identify the colour
> > + * components in the tuning data and validate the size of the loaded gains
> > + * tables.
> > + *
> > + * \return 0 on success, a negative error number otherwise
> > + */
> > +
> > +/**
> > + * \fn LscImplementation::resampleLscData
> > + * \brief Re-sample the lsc components for \a cropRectangle
> > + * \param[in] cropRectangle The sensor analogue crop rectangle
> > + * \param[in] xPos List of horizontal positions of the lsc grid nodes
> > + * \param[in] yPos List of vertical positions of the lsc grid nodes
> > + *
> > + * Lsc tables are expressed in two formats:
> > + * - A list of gain values (LscGrid)
> > + * - A radial polynomial (LscPolynomial)
> > + *
> > + * Grid-based lsc data are generated using an image at a fixed resolution and
> > + * can't at the moment be re-sampled when a different resolution is used for a
> > + * streaming session. Re-sampling a grid lsc table will return the same table
> > + * as loaded from the tuning file.
> > + *
> > + * Polynomial are more flexible and can be re-sampled for a given resolution
> > + * using a list of horizontal and vertical nodes that define the lsc grid.
> > + * Polynomial lsc tables have to be re-sampled every time a new configuration is
> > + * applied, as each streaming session might use a different sensor crop
> > + * rectangle.
> > + *
> > + * \a cropRectangle represents the size of the frame on which the polynomial Lsc
> > + * has to be re-sampled on.
> > + *
> > + * \a xPos and \a yPos represent the position of the grid nodes vertexes in
> > + * the [0, 1] interval. In example an equally spaced grid of 16 nodes will have
> > + * each segment of size 0.0625 and the list of nodes position will be
> > + * [0, 0.0625, 0.125, 0.1875, ... , 1]. It is expected that the first position
> > + * is 0 and the last position is 1.
> > + */
> > +
> > +} /* namespace ipa */
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
> > new file mode 100644
> > index 000000000000..aecae305aa9c
> > --- /dev/null
> > +++ b/src/ipa/libipa/lsc_base.h
> > @@ -0,0 +1,59 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2026 Ideas on Board Oy
> > + *
> > + * Base classes and types for LSC algorithms implementations
> > + */
> > +
> > +#pragma once
> > +
> > +#include <map>
> > +#include <string>
> > +#include <vector>
> > +
> > +#include <libcamera/geometry.h>
> > +
> > +#include "libcamera/internal/value_node.h"
> > +
> > +namespace libcamera {
> > +
> > +namespace ipa {
> > +
> > +namespace lsc {
> > +
> > +template<typename T>
> > +class Components : public std::map<std::string, std::vector<T>>
> > +{
>
> I'm a bit uneasy here. On the one hand we are going great lengths to
> write super optimized code striving for static allocations as much as
> possible and now we add a string based map lookup that is run per frame.

How are 'string based lookup' and 'static memory allocation' related ?

> On the other hand I don't actually know how expensive the lookup is.

So, Component is used at init to parse the tuning file. Fine.

At configure a set of Components is re-sampled (polynomial) or
returned as parsed from config file. Fine.

Now, the run-time.

I presume you're thinking at (using rkisp1 as an example)

 void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
-                                     const Components &set)
+                                     const ipa::lsc::Components<uint16_t> &set)
 {
-       std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
-       std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
-       std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
-       std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
+       const auto &r = set.at("r");
+       std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
+       const auto &gr = set.at("gr");
+       std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
+       const auto &gb = set.at("gb");
+       std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
+       const auto &b = set.at("b");
+       std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
 }

yes, we do a lookup on a string.

The existing implementation stores the Components vectors in a struct

	struct Components {
		uint32_t ct;
		std::vector<uint16_t> r;
		std::vector<uint16_t> gr;
		std::vector<uint16_t> gb;
		std::vector<uint16_t> b;
	};

This patch instead stores them in a map.

Now, not an STL guru here, but I presume that if your only concern is
the lookup, a map with 4 elements top shouldn't be that hard.

If there are implications I'm missing about storing the vectors in the
map instead of storing them in a struct I would be glad to learn about
that.

>
> I was wondering if it is possible to template that using a static set as
> temüplate param and a fixed size array as container but then you can't
> configure that at init() time and end up with too much template
> magic....no
>
> As a compromise, would it make sense to index the map by an enum, so
> that we could define an enum for the possible types (it will most likely
> ever only be g,r,b,rg,bg (possible ir in the future). That would at
> least replace the string lookup by an int lookup, and we'd only have
> strings at yaml parse time?

So is the only concern really comparing strings vs comparing integers
when looking up a key ? In the same code path that memcpys thousands
of values to program the lsc tables ?

I feel like I'm missing something.

>
> > +};
> > +
> > +template<typename T>
> > +class ComponentsMap : public std::map<unsigned int, Components<T>>
> > +{
> > +};
> > +
> > +} /* namespace lsc */
> > +
> > +struct LscDescriptor {
> > +       std::vector<std::string> keys;
> > +       unsigned int numHCells;
> > +       unsigned int numVCells;
> > +       Size sensorSize;
> > +};
> > +
> > +template<typename T, typename U>
> > +class LscImplementation
>
> On v1 Dan commented that U is not used in grid based. But it is used in
> the polynomial case. What about dropping T and defining it as
> U::QuantizedType? That would work for both cases and you get rid of one
> parameter. (And if there is a hardware that has a different register
> size than U::QuantizedType, the current solution would rely on luck,
> too.)

This one is a good suggestion.

Right now the template argument T serves only for loading values from
file in a type large enough to accommodate them

Using U::QuantizedType seems like a good idea.
We're gonna drop it anyway an move to store and load float gains from
the tuning file, but in the interim using Q::QuantizedType seems good
enough.

>
> > +{
> > +public:
> > +       virtual ~LscImplementation() = default;
> > +
> > +       virtual int parseLscData(const ValueNode &tuningData,
> > +                                const LscDescriptor &descriptor) = 0;
> > +
> > +       virtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> > +                                                     const std::vector<double> &xPos,
> > +                                                     const std::vector<double> &yPos) = 0;
> > +};
> > +
> > +} /* namespace ipa */
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/ipa/libipa/lsc_grid.cpp b/src/ipa/libipa/lsc_grid.cpp
> > new file mode 100644
> > index 000000000000..878994acc8c9
> > --- /dev/null
> > +++ b/src/ipa/libipa/lsc_grid.cpp
> > @@ -0,0 +1,33 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2026, Ideas On Board
> > + *
> > + * Grid based lens shading correction
> > + */
> > +
> > +#include "lsc_grid.h"
> > +
> > +/**
> > + * \file lsc_grid.h
> > + * \brief LscGrid class
> > + */
> > +
> > +namespace libcamera {
> > +
> > +LOG_DEFINE_CATEGORY(LscGrid)
> > +
> > +namespace ipa {
> > +
> > +/**
> > + * \class LscGrid
> > + * \brief Grid based lsc algorithm implementation
> > + *
> > + * Grid based lsc algorithm implementation. The LscGrid class implements lsc
> > + * support using tabular lsc data.
> > + *
> > + * \sa LscImplementation
> > + * \sa LscAlgorithm
> > + */
> > +
> > +} /* namespace ipa */
> > +} /* namespace libcamera */
> > diff --git a/src/ipa/libipa/lsc_grid.h b/src/ipa/libipa/lsc_grid.h
> > new file mode 100644
> > index 000000000000..e92cde95d18d
> > --- /dev/null
> > +++ b/src/ipa/libipa/lsc_grid.h
> > @@ -0,0 +1,113 @@
> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > +/*
> > + * Copyright (C) 2026, Ideas On Board
> > + *
> > + * Grid based lens shading correction
> > + */
> > +
> > +#pragma once
> > +
> > +#include <vector>
> > +
> > +#include <libcamera/base/log.h>
> > +
> > +#include "lsc_base.h"
> > +
> > +namespace libcamera {
> > +
> > +LOG_DECLARE_CATEGORY(LscGrid)
> > +
> > +namespace ipa {
> > +
> > +template<typename T, typename U>
> > +class LscGrid : public LscImplementation<T, U>
> > +{
> > +public:
> > +       ~LscGrid() {}
> > +
> > +       int parseLscData(const ValueNode &yamlSets,
> > +                        const LscDescriptor &descriptor) override
> > +       {
> > +               const auto &sets = yamlSets.asList();
> > +
> > +               for (const auto &yamlSet : sets) {
> > +                       uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> > +
> > +                       int ret = parseLscComponent(yamlSet, ct, descriptor);
> > +                       if (ret)
> > +                               return ret;
> > +               }
> > +
> > +               if (lscData_.empty()) {
> > +                       LOG(LscGrid, Error) << "Failed to load any sets";
> > +                       return -EINVAL;
> > +               }
> > +
> > +               return 0;
> > +       }
> > +
> > +       lsc::ComponentsMap<T> resampleLscData([[maybe_unused]] const Rectangle &cropRectangle,
> > +                                             [[maybe_unused]] const std::vector<double> &xPos,
> > +                                             [[maybe_unused]] const std::vector<double> &yPos) override
> > +       {
> > +               /* No resampling for grid-based LSC algorithm. */
> > +               return lscData_;
> > +       }
> > +
> > +private:
> > +       int parseLscComponent(const ValueNode &yamlSet,
> > +                             unsigned int ct, const LscDescriptor &descriptor)
> > +       {
> > +               lsc::Components<T> component;
> > +               for (auto &k : descriptor.keys) {
> > +                       auto [it, inserted] = component.emplace(
> > +                               std::piecewise_construct,
> > +                               std::forward_as_tuple(k.c_str()),
> > +                               std::forward_as_tuple(parseTable(yamlSet,
> > +                                                                k.c_str(),
> > +                                                                descriptor.numHCells,
> > +                                                                descriptor.numVCells)));
> > +                       if (!inserted || it->second.empty()) {
> > +                               LOG(LscGrid, Error)
> > +                                       << "Set " << k << " for color temperature "
> > +                                       << ct << " is missing";
> > +                               return -EINVAL;
> > +                       }
> > +               }
> > +
> > +               auto [it, inserted] = lscData_.emplace(ct, component);
> > +               if (!inserted) {
> > +                       LOG(LscGrid, Error)
> > +                               << "Multiple sets found for color temperature "
> > +                               << ct;
> > +                       return -EINVAL;
> > +               }
> > +
> > +               return 0;
> > +       }
> > +
> > +       std::vector<T> parseTable(const ValueNode &tuningData,
> > +                                 const char *prop, unsigned int numHCells,
> > +                                 unsigned int numVCells)
> > +       {
> > +               unsigned int kLscNumSamples = numHCells * numVCells;
> > +
> > +               std::vector<T> table =
> > +                       tuningData[prop].get<std::vector<T>>().value_or(utils::defopt);
> > +               if (table.size() != kLscNumSamples) {
> > +                       LOG(LscGrid, Error)
> > +                               << "Invalid '" << prop << "' values: expected "
> > +                               << kLscNumSamples
> > +                               << " elements, got " << table.size();
> > +                       return {};
> > +               }
> > +
> > +               return table;
> > +       }
> > +
> > +       lsc::ComponentsMap<T> lscData_;
> > +};
> > +
> > +} /* namespace ipa */
> > +
> > +} /* namespace libcamera */
> > diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
> > index f607d86c54c3..8d3f1883d6a5 100644
> > --- a/src/ipa/libipa/lsc_polynomial.cpp
> > +++ b/src/ipa/libipa/lsc_polynomial.cpp
> > @@ -1,12 +1,14 @@
> >  /* SPDX-License-Identifier: LGPL-2.1-or-later */
> >  /*
> > - * Copyright (C) 2024, Ideas On Board
> > + * Copyright (C) 2026, Ideas On Board
> >   *
> > - * Polynomial class to represent lens shading correction
> > + * Polynomial based lens shading correction
> >   */
> >
> >  #include "lsc_polynomial.h"
> >
> > +#include <assert.h>
> > +
> >  #include <libcamera/base/log.h>
> >
> >  /**
> > @@ -21,7 +23,7 @@ LOG_DEFINE_CATEGORY(LscPolynomial)
> >  namespace ipa {
> >
> >  /**
> > - * \class LscPolynomial
> > + * \class Polynomial
> >   * \brief Class for handling even polynomials used in lens shading correction
> >   *
> >   * Shading artifacts of camera lenses can be modeled using even radial
> > @@ -31,7 +33,7 @@ namespace ipa {
> >   */
> >
> >  /**
> > - * \fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > + * \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> >                       double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> >                       double k4 = 0.0)
> >   * \brief Construct a polynomial using the given coefficients
> > @@ -45,7 +47,13 @@ namespace ipa {
> >   */
> >
> >  /**
> > - * \fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)
> > + * \fn Polynomial::Polynomial(const Polynomial &other)
> > + * \brief Construct a Polynomial by copy
> > + * \param[in] other The Polynomial to copy construct from
> > + */
> > +
> > +/**
> > + * \fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)
> >   * \brief Sample the polynomial at the given normalized pixel position
> >   *
> >   * This functions samples the polynomial at the given pixel position divided by
> > @@ -55,9 +63,21 @@ namespace ipa {
> >   * \param y y position in normalized coordinates
> >   * \return The sampled value
> >   */
> > +double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
> > +{
> > +       double dx = x - cnx_;
> > +       double dy = y - cny_;
> > +       double r = sqrt(dx * dx + dy * dy);
> > +       double res = 1.0;
> > +
> > +       for (unsigned int i = 0; i < coefficients_.size(); i++)
> > +               res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> > +
> > +       return res;
> > +}
>
> These are plain refactorings. I see that the whole libipa rework
> is difficult to do in small patches, but I think it would make review
> (and later history reading) easier if we'd cut out these refactorings
> into separate preparatory patches.

I think we all knew that this work was going to be hard to review as
it was hard to implement.

I again suggest to look at the final result more than the diff.

I could break the patches down in small non-compilable units if it
helps.

>
> >
> >  /**
> > - * \fn LscPolynomial::getM()
> > + * \fn Polynomial::getM()
> >   * \brief Get the value m as described in the dng specification
> >   *
> >   * Returns m according to dng spec. m represents the Euclidean distance
> > @@ -66,9 +86,18 @@ namespace ipa {
> >   *
> >   * \return The sampled value
> >   */
> > +double Polynomial::getM() const
> > +{
> > +       double cpx = imageSize_.width * cx_;
> > +       double cpy = imageSize_.height * cy_;
> > +       double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
> > +       double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
> > +
> > +       return sqrt(mx * mx + my * my);
> > +}
> >
> >  /**
> > - * \fn LscPolynomial::setReferenceImageSize(const Size &size)
> > + * \fn Polynomial::setReferenceImageSize(const Size &size)
> >   * \brief Set the reference image size
> >   *
> >   * Set the reference image size that is used for subsequent calls to getM() and
> > @@ -76,6 +105,92 @@ namespace ipa {
> >   *
> >   * \param size The size of the reference image
> >   */
> > +void Polynomial::setReferenceImageSize(const Size &size)
> > +{
> > +       assert(!size.isNull());
> > +       imageSize_ = size;
> > +
> > +       /* Calculate normalized centers */
> > +       double m = getM();
> > +       cnx_ = (size.width * cx_) / m;
> > +       cny_ = (size.height * cy_) / m;
> > +}
> > +
> > +/**
> > + * \class LscPolynomialBase
> > + * \brief Base class for LscPolynomial
> > + *
> > + * Base class for LscPolynomial for non-templated functions.
> > + */
> > +
> > +/**
> > + * \brief Parse polynomial lsc data
> > + * \param[in] yamlSets The tuning file content
> > + * \param[in] descriptor The lsc engine descriptor
> > + *
> > + * Parse the lsc data in polyomial form from the \a yamlSet tuning data.
> > + */
> > +int LscPolynomialBase::parseLscData(const ValueNode &yamlSets,
> > +                                   const LscDescriptor &descriptor)
> > +{
> > +       const auto &sets = yamlSets.asList();
> > +       for (const auto &yamlSet : sets) {
> > +               uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> > +
> > +               Components components;
> > +               for (auto &k : descriptor.keys) {
> > +                       auto polynomial = yamlSet[k.c_str()].get<Polynomial>();
> > +                       if (!polynomial) {
> > +                               LOG(LscPolynomial, Error)
> > +                                       << "Missing polynomial for component "
> > +                                       << k;
> > +                               return -EINVAL;
> > +                       }
> > +
> > +                       auto [it, inserted] =
> > +                               components.emplace(std::piecewise_construct,
> > +                                                  std::forward_as_tuple(k.c_str()),
> > +                                                  std::forward_as_tuple(*polynomial));
> > +
> > +                       it->second.setReferenceImageSize(descriptor.sensorSize);
> > +               }
> > +
> > +               auto [it, inserted] = lscData_.emplace(ct, components);
> > +               if (!inserted) {
> > +                       LOG(LscPolynomial, Error)
> > +                               << "Multiple sets found for "
> > +                               << "color temperature " << ct;
> > +                       return -EINVAL;
> > +               }
> > +       }
> > +
> > +       if (lscData_.empty()) {
> > +               LOG(LscPolynomial, Error) << "Failed to load any sets";
> > +               return -EINVAL;
> > +       }
> > +
> > +       return 0;
> > +}
> > +
> > +/**
> > + * \var LscPolynomialBase::lscData_
> > + * \brief The polynomial lsc data
> > + *
> > + * Maps colour temperatures to per-colour radial polynomial definitions.
> > + */
> > +
> > +/**
> > + * \class LscPolynomial
> > + * \brief Radial Polynomial lsc algorithm implementation
> > + *
> > + * Polynomial-based lsc algorithm implementation. The LscPolynomial class
> > + * implements lsc support using Polynomial to represent the shading artifacts
> > + * map.
> > + *
> > + * \sa LscImplementation
> > + * \sa LscAlgorithm
> > + *
> > + */
> >
> > -} // namespace ipa
> > -} // namespace libcamera
> > +} /* namespace ipa */
> > +} /* namespace libcamera */
> > diff --git a/src/ipa/libipa/lsc_polynomial.h b/src/ipa/libipa/lsc_polynomial.h
> > index d7d9ae42e360..72cca591f260 100644
> > --- a/src/ipa/libipa/lsc_polynomial.h
> > +++ b/src/ipa/libipa/lsc_polynomial.h
> > @@ -1,15 +1,17 @@
> >  /* SPDX-License-Identifier: LGPL-2.1-or-later */
> >  /*
> > - * Copyright (C) 2024, Ideas On Board
> > + * Copyright (C) 2026, Ideas On Board
> >   *
> > - * Helper for radial polynomial used in lens shading correction.
> > + * Polynomial based lens shading correction
> >   */
> > +
> >  #pragma once
> >
> >  #include <algorithm>
> >  #include <array>
> > -#include <assert.h>
> >  #include <cmath>
> > +#include <optional>
> > +#include <tuple>
> >
> >  #include <libcamera/base/log.h>
> >  #include <libcamera/base/span.h>
> > @@ -18,64 +20,117 @@
> >
> >  #include "libcamera/internal/value_node.h"
> >
> > +#include "lsc_base.h"
> > +
> >  namespace libcamera {
> >
> >  LOG_DECLARE_CATEGORY(LscPolynomial)
> >
> >  namespace ipa {
> >
> > -class LscPolynomial
> > +class Polynomial
>
> If we rename this class it should be moved into the ipa::lsc::
> namespace as it is not a generic polynomial class.

I think I can do that

>
> >  {
> >  public:
> > -       LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > -                     double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> > -                     double k4 = 0.0)
> > +       Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > +                  double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> > +                  double k4 = 0.0)
> >                 : cx_(cx), cy_(cy), cnx_(0), cny_(0),
> >                   coefficients_({ k0, k1, k2, k3, k4 })
> >         {
> >         }
> >
> > -       double sampleAtNormalizedPixelPos(double x, double y) const
> > +       Polynomial(const Polynomial &other) = default;
> > +
> > +       double sampleAtNormalizedPixelPos(double x, double y) const;
> > +       double getM() const;
> > +       void setReferenceImageSize(const Size &size);
> > +
> > +private:
> > +       double cx_;
> > +       double cy_;
> > +       double cnx_;
> > +       double cny_;
> > +       std::array<double, 5> coefficients_;
> > +       Size imageSize_;
> > +};
> > +
> > +class LscPolynomialBase
> > +{
> > +private:
> > +       using Components = std::map<std::string, Polynomial>;
>
> > +       using ComponentsMap = std::map<unsigned int, Components>;
> > +
> > +public:
> > +       int parseLscData(const ValueNode &yamlSets,
> > +                        const LscDescriptor &descriptor);
> > +
> > +protected:
> > +       ComponentsMap lscData_;
> > +};
> > +
> > +template<typename T, typename U>
> > +class LscPolynomial : public LscPolynomialBase, public LscImplementation<T, U>
> > +{
> > +public:
> > +       ~LscPolynomial() {}
> > +
> > +       int parseLscData(const ValueNode &yamlSets,
> > +                        const LscDescriptor &descriptor) override
> >         {
> > -               double dx = x - cnx_;
> > -               double dy = y - cny_;
> > -               double r = sqrt(dx * dx + dy * dy);
> > -               double res = 1.0;
> > -               for (unsigned int i = 0; i < coefficients_.size(); i++) {
> > -                       res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> > -               }
> > -               return res;
> > +               return LscPolynomialBase::parseLscData(yamlSets, descriptor);
> >         }
> >
> > -       double getM() const
> > +       lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> > +                                             const std::vector<double> &xPos,
> > +                                             const std::vector<double> &yPos) override
> >         {
> > -               double cpx = imageSize_.width * cx_;
> > -               double cpy = imageSize_.height * cy_;
> > -               double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
> > -               double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
> > +               lsc::ComponentsMap<T> components;
> >
> > -               return sqrt(mx * mx + my * my);
> > -       }
> > +               for (auto &[t, c] : lscData_) {
> > +                       lsc::Components<T> comp;
> >
> > -       void setReferenceImageSize(const Size &size)
> > -       {
> > -               assert(!size.isNull());
> > -               imageSize_ = size;
> > +                       for (auto &[k, p] : c) {
> > +                               comp.emplace(std::piecewise_construct,
> > +                                            std::forward_as_tuple(k),
> > +                                            std::forward_as_tuple(samplePolynomial(p, xPos, yPos,
> > +                                                                                   cropRectangle)));
> > +                       }
> >
> > -               /* Calculate normalized centers */
> > -               double m = getM();
> > -               cnx_ = (size.width * cx_) / m;
> > -               cny_ = (size.height * cy_) / m;
> > +                       components[t] = comp;
> > +               }
> > +
> > +               return components;
> >         }
> >
> >  private:
> > -       double cx_;
> > -       double cy_;
> > -       double cnx_;
> > -       double cny_;
> > -       std::array<double, 5> coefficients_;
> > +       std::vector<T> samplePolynomial(const Polynomial &poly,
> > +                                       Span<const double> xPositions,
> > +                                       Span<const double> yPositions,
> > +                                       const Rectangle &cropRectangle)
> >
> > -       Size imageSize_;
> > +       {
> > +               double m = poly.getM();
> > +               double x0 = cropRectangle.x / m;
> > +               double y0 = cropRectangle.y / m;
> > +               double w = cropRectangle.width / m;
> > +               double h = cropRectangle.height / m;
> > +               std::vector<T> samples;
> > +
> > +               samples.reserve(xPositions.size() * yPositions.size());
> > +
> > +               for (double y : yPositions) {
> > +                       for (double x : xPositions) {
> > +                               double xp = x0 + x * w;
> > +                               double yp = y0 + y * h;
> > +                               float sample = static_cast<float>
> > +                                               (poly.sampleAtNormalizedPixelPos(xp, yp));
> > +
> > +                               samples.push_back(U(sample).quantized());
> > +                       }
> > +               }
> > +
> > +               return samples;
> > +       }
> >  };
> >
> >  } /* namespace ipa */
> > @@ -83,8 +138,8 @@ private:
> >  #ifndef __DOXYGEN__
> >
> >  template<>
> > -struct ValueNode::Accessor<ipa::LscPolynomial> {
> > -       std::optional<ipa::LscPolynomial> get(const ValueNode &obj) const
> > +struct ValueNode::Accessor<ipa::Polynomial> {
> > +       std::optional<ipa::Polynomial> get(const ValueNode &obj) const
> >         {
> >                 std::optional<double> cx = obj["cx"].get<double>();
> >                 std::optional<double> cy = obj["cy"].get<double>();
> > @@ -98,7 +153,7 @@ struct ValueNode::Accessor<ipa::LscPolynomial> {
> >                         LOG(LscPolynomial, Error)
> >                                 << "Polynomial is missing a parameter";
> >
> > -               return ipa::LscPolynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);
> > +               return ipa::Polynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);
> >         }
> >  };
> >
> > diff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build
> > index edf8eabd8b78..da6ea0c5e130 100644
> > --- a/src/ipa/libipa/meson.build
> > +++ b/src/ipa/libipa/meson.build
> > @@ -14,6 +14,9 @@ libipa_headers = files([
> >      'fixedpoint.h',
> >      'histogram.h',
> >      'interpolator.h',
> > +    'lsc.h',
> > +    'lsc_base.h',
> > +    'lsc_grid.h',
> >      'lsc_polynomial.h',
> >      'lux.h',
> >      'module.h',
> > @@ -36,6 +39,8 @@ libipa_sources = files([
> >      'fixedpoint.cpp',
> >      'histogram.cpp',
> >      'interpolator.cpp',
> > +    'lsc.cpp',
> > +    'lsc_grid.cpp',
> >      'lsc_polynomial.cpp',
> >      'lux.cpp',
> >      'module.cpp',
> > diff --git a/src/ipa/rkisp1/algorithms/lsc.cpp b/src/ipa/rkisp1/algorithms/lsc.cpp
> > index 25f7c512b35c..51076737ffa5 100644
> > --- a/src/ipa/rkisp1/algorithms/lsc.cpp
> > +++ b/src/ipa/rkisp1/algorithms/lsc.cpp
> > @@ -2,7 +2,7 @@
> >  /*
> >   * Copyright (C) 2021-2022, Ideas On Board
> >   *
> > - * RkISP1 Lens Shading Correction control
> > + * RkISP1 Lens Shading Correction algorithm
> >   */
> >
> >  #include "lsc.h"
> > @@ -14,56 +14,17 @@
> >  #include <libcamera/base/log.h>
> >  #include <libcamera/base/utils.h>
> >
> > -#include "libcamera/internal/value_node.h"
> > -
> > -#include "libipa/lsc_polynomial.h"
> > -#include "linux/rkisp1-config.h"
> > -
> >  /**
> >   * \file lsc.h
> >   */
> >
> >  namespace libcamera {
> >
> > -namespace ipa {
> > -
> > -template<typename T>
> > -void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,
> > -                      std::vector<T> &dest, double lambda)
> > -{
> > -       ASSERT(a.size() == b.size());
> > -       dest.resize(a.size());
> > -       for (size_t i = 0; i < a.size(); i++)
> > -               dest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;
> > -}
> > -
> > -template<>
> > -void Interpolator<rkisp1::algorithms::LensShadingCorrection::Components>::
> > -       interpolate(const rkisp1::algorithms::LensShadingCorrection::Components &a,
> > -                   const rkisp1::algorithms::LensShadingCorrection::Components &b,
> > -                   rkisp1::algorithms::LensShadingCorrection::Components &dest,
> > -                   double lambda)
> > -{
> > -       interpolateVector(a.r, b.r, dest.r, lambda);
> > -       interpolateVector(a.gr, b.gr, dest.gr, lambda);
> > -       interpolateVector(a.gb, b.gb, dest.gb, lambda);
> > -       interpolateVector(a.b, b.b, dest.b, lambda);
> > -}
> > -
> > -} /* namespace ipa */
> > -
> >  namespace ipa::rkisp1::algorithms {
> >
> >  /**
> >   * \class LensShadingCorrection
> >   * \brief RkISP1 Lens Shading Correction control
> > - *
> > - * Due to the optical characteristics of the lens, the light intensity received
> > - * by the sensor is not uniform.
> > - *
> > - * The Lens Shading Correction algorithm applies multipliers to all pixels
> > - * to compensate for the lens shading effect. The coefficients are
> > - * specified in a downscaled table in the YAML tuning file.
> >   */
> >
> >  LOG_DEFINE_CATEGORY(RkISP1Lsc)
> > @@ -72,265 +33,20 @@ namespace {
> >
> >  constexpr int kColourTemperatureQuantization = 10;
> >
> > -class LscPolynomialShadingDescriptor : public LensShadingCorrection::ShadingDescriptor
> > -{
> > -public:
> > -       LscPolynomialShadingDescriptor(const LscPolynomial &pr, const LscPolynomial &pgr,
> > -                                      const LscPolynomial &pgb, const LscPolynomial &pb)
> > -               : pr_(pr), pgr_(pgr), pgb_(pgb), pb_(pb)
> > -       {
> > -       }
> > -
> > -       LensShadingCorrection::Components sampleForCrop(const Rectangle &cropRectangle,
> > -                                                       Span<const double> xSizes,
> > -                                                       Span<const double> ySizes) override;
> > -
> > -private:
> > -       std::vector<uint16_t> samplePolynomial(const LscPolynomial &poly,
> > -                                              Span<const double> xPositions,
> > -                                              Span<const double> yPositions,
> > -                                              const Rectangle &cropRectangle);
> > -
> > -       std::vector<double> sizesListToPositions(Span<const double> sizes);
> > -
> > -       LscPolynomial pr_;
> > -       LscPolynomial pgr_;
> > -       LscPolynomial pgb_;
> > -       LscPolynomial pb_;
> > -};
> > -
> > -LensShadingCorrection::Components
> > -LscPolynomialShadingDescriptor::sampleForCrop(const Rectangle &cropRectangle,
> > -                                             Span<const double> xSizes,
> > -                                             Span<const double> ySizes)
> > -{
> > -       std::vector<double> xPos = sizesListToPositions(xSizes);
> > -       std::vector<double> yPos = sizesListToPositions(ySizes);
> > -
> > -       return {
> > -               .r = samplePolynomial(pr_, xPos, yPos, cropRectangle),
> > -               .gr = samplePolynomial(pgr_, xPos, yPos, cropRectangle),
> > -               .gb = samplePolynomial(pgb_, xPos, yPos, cropRectangle),
> > -               .b = samplePolynomial(pb_, xPos, yPos, cropRectangle)
> > -       };
> > -}
> > -
> > -std::vector<uint16_t>
> > -LscPolynomialShadingDescriptor::samplePolynomial(const LscPolynomial &poly,
> > -                                                Span<const double> xPositions,
> > -                                                Span<const double> yPositions,
> > -                                                const Rectangle &cropRectangle)
> > -{
> > -       double m = poly.getM();
> > -       double x0 = cropRectangle.x / m;
> > -       double y0 = cropRectangle.y / m;
> > -       double w = cropRectangle.width / m;
> > -       double h = cropRectangle.height / m;
> > -       std::vector<uint16_t> samples;
> > -
> > -       samples.reserve(xPositions.size() * yPositions.size());
> > -
> > -       for (double y : yPositions) {
> > -               for (double x : xPositions) {
> > -                       double xp = x0 + x * w;
> > -                       double yp = y0 + y * h;
> > -                       /*
> > -                        * The hardware uses 2.10 fixed point format and limits
> > -                        * the legal values to [1..3.999]. Scale and clamp the
> > -                        * sampled value accordingly.
> > -                        */
> > -                       int v = static_cast<int>(
> > -                               poly.sampleAtNormalizedPixelPos(xp, yp) *
> > -                               1024);
> > -                       v = std::clamp(v, 1024, 4095);
> > -                       samples.push_back(v);
> > -               }
> > -       }
> > -       return samples;
> > -}
> > -
> > -/*
> > - * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left
> > - * quadrant of the grid. This is then mirrored in hardware to the other
> > - * quadrants. See parseSizes() for further details. For easier handling, this
> > - * function converts the cell sizes of half the grid to a list of position of
> > - * the whole grid (on one axis). Example:
> > - *
> > - * input:   | 0.2 | 0.3 |
> > - * output: 0.0   0.2   0.5   0.8   1.0
> > - */
> > -std::vector<double>
> > -LscPolynomialShadingDescriptor::sizesListToPositions(Span<const double> sizes)
> > -{
> > -       const int half = sizes.size();
> > -       std::vector<double> positions(half * 2 + 1);
> > -       double x = 0.0;
> > -
> > -       positions[half] = 0.5;
> > -       for (int i = 1; i <= half; i++) {
> > -               x += sizes[half - i];
> > -               positions[half - i] = 0.5 - x;
> > -               positions[half + i] = 0.5 + x;
> > -       }
> > -
> > -       return positions;
> > -}
> > -
> > -class LscPolynomialLoader
> > -{
> > -public:
> > -       LscPolynomialLoader(const Size &sensorSize)
> > -               : sensorSize_(sensorSize)
> > -       {
> > -       }
> > -
> > -       int parseLscData(const ValueNode &yamlSets,
> > -                        LensShadingCorrection::ShadingDescriptorMap &lscData);
> > -
> > -private:
> > -       Size sensorSize_;
> > -};
> > -
> > -int LscPolynomialLoader::parseLscData(const ValueNode &yamlSets,
> > -                                     LensShadingCorrection::ShadingDescriptorMap &lscData)
> > +unsigned int quantize(unsigned int value, unsigned int step)
> >  {
> > -       const auto &sets = yamlSets.asList();
> > -       for (const auto &yamlSet : sets) {
> > -               std::optional<LscPolynomial> pr, pgr, pgb, pb;
> > -               uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> > -
> > -               if (lscData.count(ct)) {
> > -                       LOG(RkISP1Lsc, Error)
> > -                               << "Multiple sets found for "
> > -                               << "color temperature " << ct;
> > -                       return -EINVAL;
> > -               }
> > -
> > -               pr = yamlSet["r"].get<LscPolynomial>();
> > -               pgr = yamlSet["gr"].get<LscPolynomial>();
> > -               pgb = yamlSet["gb"].get<LscPolynomial>();
> > -               pb = yamlSet["b"].get<LscPolynomial>();
> > -
> > -               if (!(pr || pgr || pgb || pb)) {
> > -                       LOG(RkISP1Lsc, Error)
> > -                               << "Failed to parse polynomial for "
> > -                               << "colour temperature " << ct;
> > -                       return -EINVAL;
> > -               }
> > -
> > -               pr->setReferenceImageSize(sensorSize_);
> > -               pgr->setReferenceImageSize(sensorSize_);
> > -               pgb->setReferenceImageSize(sensorSize_);
> > -               pb->setReferenceImageSize(sensorSize_);
> > -
> > -               lscData.emplace(
> > -                       ct, std::make_unique<LscPolynomialShadingDescriptor>(
> > -                                   *pr, *pgr, *pgb, *pb));
> > -       }
> > -
> > -       if (lscData.empty()) {
> > -               LOG(RkISP1Lsc, Error) << "Failed to load any sets";
> > -               return -EINVAL;
> > -       }
> > -
> > -       return 0;
> > +       return std::lround(value / static_cast<double>(step)) * step;
> >  }
> >
> > -class LscTableShadingDescriptor : public LensShadingCorrection::ShadingDescriptor
> > -{
> > -public:
> > -       LscTableShadingDescriptor(LensShadingCorrection::Components components)
> > -               : lscData_(std::move(components))
> > -       {
> > -       }
> > -
> > -       LensShadingCorrection::Components
> > -       sampleForCrop([[maybe_unused]] const Rectangle &cropRectangle,
> > -                     [[maybe_unused]] Span<const double> xSizes,
> > -                     [[maybe_unused]] Span<const double> ySizes) override
> > -       {
> > -               LOG(RkISP1Lsc, Warning)
> > -                       << "Tabular LSC data doesn't support resampling";
> > -               return lscData_;
> > -       }
> > -
> > -private:
> > -       LensShadingCorrection::Components lscData_;
> > -};
> > -
> > -class LscTableLoader
> > -{
> > -public:
> > -       int parseLscData(const ValueNode &yamlSets,
> > -                        LensShadingCorrection::ShadingDescriptorMap &lscData);
> > -
> > -private:
> > -       std::vector<uint16_t> parseTable(const ValueNode &tuningData,
> > -                                        const char *prop);
> > -};
> > -
> > -int LscTableLoader::parseLscData(const ValueNode &yamlSets,
> > -                                LensShadingCorrection::ShadingDescriptorMap &lscData)
> > -{
> > -       const auto &sets = yamlSets.asList();
> > -
> > -       for (const auto &yamlSet : sets) {
> > -               uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
> > -
> > -               if (lscData.count(ct)) {
> > -                       LOG(RkISP1Lsc, Error)
> > -                               << "Multiple sets found for color temperature "
> > -                               << ct;
> > -                       return -EINVAL;
> > -               }
> > -
> > -               LensShadingCorrection::Components set;
> > -               set.r = parseTable(yamlSet, "r");
> > -               set.gr = parseTable(yamlSet, "gr");
> > -               set.gb = parseTable(yamlSet, "gb");
> > -               set.b = parseTable(yamlSet, "b");
> > -
> > -               if (set.r.empty() || set.gr.empty() ||
> > -                   set.gb.empty() || set.b.empty()) {
> > -                       LOG(RkISP1Lsc, Error)
> > -                               << "Set for color temperature " << ct
> > -                               << " is missing tables";
> > -                       return -EINVAL;
> > -               }
> > -
> > -               lscData.emplace(
> > -                       ct, std::make_unique<LscTableShadingDescriptor>(std::move(set)));
> > -       }
> > -
> > -       if (lscData.empty()) {
> > -               LOG(RkISP1Lsc, Error) << "Failed to load any sets";
> > -               return -EINVAL;
> > -       }
> > -
> > -       return 0;
> > -}
> > +} /* namespace */
> >
> > -std::vector<uint16_t> LscTableLoader::parseTable(const ValueNode &tuningData,
> > -                                                const char *prop)
> > +LensShadingCorrection::LensShadingCorrection()
> > +       : lastAppliedCt_(0), lastAppliedQuantizedCt_(0)
> >  {
> > -       static constexpr unsigned int kLscNumSamples =
> > -               RKISP1_CIF_ISP_LSC_SAMPLES_MAX * RKISP1_CIF_ISP_LSC_SAMPLES_MAX;
> > -
> > -       std::vector<uint16_t> table =
> > -               tuningData[prop].get<std::vector<uint16_t>>().value_or(utils::defopt);
> > -       if (table.size() != kLscNumSamples) {
> > -               LOG(RkISP1Lsc, Error)
> > -                       << "Invalid '" << prop << "' values: expected "
> > -                       << kLscNumSamples
> > -                       << " elements, got " << table.size();
> > -               return {};
> > -       }
> > -
> > -       return table;
> >  }
> >
> > -std::vector<double> parseSizes(const ValueNode &tuningData,
> > -                              const char *prop)
> > +std::vector<double> LensShadingCorrection::parseSizes(const ValueNode &tuningData,
> > +                                                     const char *prop)
> >  {
> >         std::vector<double> sizes =
> >                 tuningData[prop].get<std::vector<double>>().value_or(utils::defopt);
> > @@ -359,22 +75,36 @@ std::vector<double> parseSizes(const ValueNode &tuningData,
> >         return sizes;
> >  }
> >
> > -unsigned int quantize(unsigned int value, unsigned int step)
> > +/*
> > + * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left
> > + * quadrant of the grid. This is then mirrored in hardware to the other
> > + * quadrants. See parseSizes() for further details. For easier handling, this
> > + * function converts the cell sizes of half the grid to a list of position of
> > + * the whole grid (on one axis). Example:
> > + *
> > + * input:   | 0.2 | 0.3 |
> > + * output: 0.0   0.2   0.5   0.8   1.0
> > + */
> > +std::vector<double> LensShadingCorrection::sizesToPositions(Span<const double> sizes)
> >  {
> > -       return std::lround(value / static_cast<double>(step)) * step;
> > -}
> > +       const int half = sizes.size();
> > +       std::vector<double> positions(half * 2 + 1);
> > +       double x = 0.0;
> >
> > -} /* namespace */
> > +       positions[half] = 0.5;
> > +       for (int i = 1; i <= half; i++) {
> > +               x += sizes[half - i];
> > +               positions[half - i] = 0.5 - x;
> > +               positions[half + i] = 0.5 + x;
> > +       }
> >
> > -LensShadingCorrection::LensShadingCorrection()
> > -       : lastAppliedCt_(0), lastAppliedQuantizedCt_(0)
> > -{
> > +       return positions;
> >  }
> >
> >  /**
> >   * \copydoc libcamera::ipa::Algorithm::init
> >   */
> > -int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
> > +int LensShadingCorrection::init(IPAContext &context,
> >                                 const ValueNode &tuningData)
> >  {
> >         xSize_ = parseSizes(tuningData, "x-size");
> > @@ -383,56 +113,27 @@ int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
> >         if (xSize_.empty() || ySize_.empty())
> >                 return -EINVAL;
> >
> > -       /* Get all defined sets to apply. */
> > -       const ValueNode &yamlSets = tuningData["sets"];
> > -       if (!yamlSets.isList()) {
> > -               LOG(RkISP1Lsc, Error)
> > -                       << "'sets' parameter not found in tuning file";
> > -               return -EINVAL;
> > -       }
> > -
> > -       ShadingDescriptorMap lscData;
> > -       int ret = 0;
> > -
> > -       std::string type = tuningData["type"].get<std::string>("table");
> > -       if (type == "table") {
> > -               LOG(RkISP1Lsc, Debug) << "Loading tabular LSC data.";
> > -               auto loader = LscTableLoader();
> > -               ret = loader.parseLscData(yamlSets, lscData);
> > -       } else if (type == "polynomial") {
> > -               LOG(RkISP1Lsc, Debug) << "Loading polynomial LSC data.";
> > -               /*
> > -                * \todo: Most likely the reference frame should be native_size.
> > -                * Let's wait how the internal discussions progress.
> > -                */
> > -               auto loader = LscPolynomialLoader(context.sensorInfo.activeAreaSize);
> > -               ret = loader.parseLscData(yamlSets, lscData);
> > -       } else {
> > -               LOG(RkISP1Lsc, Error) << "Unsupported LSC data type '"
> > -                                     << type << "'";
> > -               ret = -EINVAL;
> > -       }
> > -
> > -       if (ret)
> > -               return ret;
> > +       xPos_ = sizesToPositions(xSize_);
> > +       yPos_ = sizesToPositions(ySize_);
> >
> > -       context.ctrlMap[&controls::LensShadingCorrectionEnable] =
> > -               ControlInfo(false, true, true);
> > -
> > -       shadingDescriptors_ = std::move(lscData);
> > -
> > -       return 0;
> > +       return lscAlgo_.init(tuningData, context.ctrlMap, {
> > +                               .keys = { "r", "gr", "gb", "b" },
> > +                               .numHCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,
> > +                               .numVCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,
> > +                               .sensorSize = context.sensorInfo.activeAreaSize
> > +                            });
> >  }
> >
> >  /**
> >   * \copydoc libcamera::ipa::Algorithm::configure
> >   */
> >  int LensShadingCorrection::configure(IPAContext &context,
> > -                                    [[maybe_unused]] const IPACameraSensorInfo &configInfo)
> > +                                    const IPACameraSensorInfo &configInfo)
> >  {
> >         const Size &size = context.configuration.sensor.size;
> >         Size totalSize{};
> >
> > +       /* Calculate gradients. */
> >         for (unsigned int i = 0; i < RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE; ++i) {
> >                 xSizes_[i] = xSize_[i] * size.width;
> >                 ySizes_[i] = ySize_[i] * size.height;
> > @@ -455,16 +156,8 @@ int LensShadingCorrection::configure(IPAContext &context,
> >                 yGrad_[i] = std::round(32768 / ySizes_[i]);
> >         }
> >
> > -       LOG(RkISP1Lsc, Debug) << "Sample LSC data for " << configInfo.analogCrop;
> > -       std::map<unsigned int, LensShadingCorrection::Components> shadingData;
> > -       for (const auto &[t, descriptor] : shadingDescriptors_)
> > -               shadingData[t] = descriptor->sampleForCrop(configInfo.analogCrop,
> > -                                                          xSize_, ySize_);
> > -
> > -       sets_.setData(std::move(shadingData));
> > -
> > -       context.activeState.lsc.enabled = true;
> > -       return 0;
> > +       return lscAlgo_.configure(context.activeState.lsc, configInfo.analogCrop,
> > +                                 xPos_, yPos_);
> >  }
> >
> >  void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)
> > @@ -476,12 +169,16 @@ void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)
> >  }
> >
> >  void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
> > -                                     const Components &set)
> > +                                     const ipa::lsc::Components<uint16_t> &set)
> >  {
> > -       std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
> > -       std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
> > -       std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
> > -       std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
> > +       const auto &r = set.at("r");
> > +       std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
> > +       const auto &gr = set.at("gr");
> > +       std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
> > +       const auto &gb = set.at("gb");
> > +       std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
> > +       const auto &b = set.at("b");
> > +       std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
> >  }
> >
> >  /**
> > @@ -492,19 +189,8 @@ void LensShadingCorrection::queueRequest(IPAContext &context,
> >                                          IPAFrameContext &frameContext,
> >                                          const ControlList &controls)
> >  {
> > -       auto &lsc = context.activeState.lsc;
> > -
> > -       const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);
> > -       if (lscEnable && *lscEnable != lsc.enabled) {
> > -               lsc.enabled = *lscEnable;
> > -
> > -               LOG(RkISP1Lsc, Debug)
> > -                       << (lsc.enabled ? "Enabling" : "Disabling") << " Lsc";
> > -
> > -               frameContext.lsc.update = true;
> > -       }
> > -
> > -       frameContext.lsc.enabled = lsc.enabled;
> > +       lscAlgo_.queueRequest(context.activeState.lsc, frameContext.lsc,
> > +                             controls);
> >  }
> >
> >  /**
> > @@ -542,7 +228,7 @@ void LensShadingCorrection::prepare([[maybe_unused]] IPAContext &context,
> >
> >         setParameters(*config);
> >
> > -       const Components &set = sets_.getInterpolated(quantizedCt);
> > +       const auto &set = lscAlgo_.interpolateComponents(quantizedCt);
> >         copyTable(*config, set);
> >
> >         lastAppliedCt_ = ct;
> > @@ -562,7 +248,7 @@ void LensShadingCorrection::process([[maybe_unused]] IPAContext &context,
> >                                     [[maybe_unused]] const rkisp1_stat_buffer *stats,
> >                                     ControlList &metadata)
> >  {
> > -       metadata.set(controls::LensShadingCorrectionEnable, frameContext.lsc.enabled);
> > +       lscAlgo_.process(frameContext.lsc, metadata);
> >  }
> >
> >  REGISTER_IPA_ALGORITHM(LensShadingCorrection, "LensShadingCorrection")
> > diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
> > index 0a256e225327..7c656d95acfb 100644
> > --- a/src/ipa/rkisp1/algorithms/lsc.h
> > +++ b/src/ipa/rkisp1/algorithms/lsc.h
> > @@ -2,17 +2,23 @@
> >  /*
> >   * Copyright (C) 2021-2022, Ideas On Board
> >   *
> > - * RkISP1 Lens Shading Correction control
> > + * RkISP1 Lens Shading Correction algorithm
> >   */
> >
> >  #pragma once
> >
> > -#include <map>
> > -#include <memory>
> > +#include <vector>
> >
> > -#include "libipa/interpolator.h"
> > +#include <linux/rkisp1-config.h>
> > +
> > +#include "libcamera/internal/value_node.h"
> > +
> > +#include "libipa/fixedpoint.h"
> > +#include "libipa/lsc.h"
> >
> >  #include "algorithm.h"
> > +#include "ipa_context.h"
> > +#include "params.h"
> >
> >  namespace libcamera {
> >
> > @@ -37,39 +43,28 @@ public:
> >                      const rkisp1_stat_buffer *stats,
> >                      ControlList &metadata) override;
> >
> > -       struct Components {
> > -               std::vector<uint16_t> r;
> > -               std::vector<uint16_t> gr;
> > -               std::vector<uint16_t> gb;
> > -               std::vector<uint16_t> b;
> > -       };
> > -
> > -       class ShadingDescriptor
> > -       {
> > -       public:
> > -               virtual ~ShadingDescriptor() = default;
> > -               virtual Components sampleForCrop(const Rectangle &cropRectangle,
> > -                                                Span<const double> xSizes,
> > -                                                Span<const double> ySizes) = 0;
> > -       };
> > -
> > -       using ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;
> > -
> >  private:
> > +       std::vector<double> parseSizes(const ValueNode &tuningData,
> > +                                      const char *prop);
> > +       std::vector<double> sizesToPositions(Span<const double> sizes);
> > +
> >         void setParameters(rkisp1_cif_isp_lsc_config &config);
> > -       void copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);
> > +       void copyTable(rkisp1_cif_isp_lsc_config &config,
> > +                      const ipa::lsc::Components<uint16_t> &set0);
> >
> > -       ShadingDescriptorMap shadingDescriptors_;
> > -       ipa::Interpolator<Components> sets_;
> >         std::vector<double> xSize_;
> >         std::vector<double> ySize_;
> >         uint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> >         uint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> >         uint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> >         uint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > +       std::vector<double> xPos_;
> > +       std::vector<double> yPos_;
> >
> >         unsigned int lastAppliedCt_;
> >         unsigned int lastAppliedQuantizedCt_;
> > +
> > +       LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;
>
> I don't really like specifying the fixed format at compile time, as we
> will have a hard time to implement support for different formats at
> runtime like Mali supports. But I also don't see an easy way out of
> that.
>
> >  };
> >
> >  } /* namespace ipa::rkisp1::algorithms */
> > diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
> > index cd97e10bcf2b..005f4102b4f6 100644
> > --- a/src/ipa/rkisp1/ipa_context.h
> > +++ b/src/ipa/rkisp1/ipa_context.h
> > @@ -30,6 +30,7 @@
> >  #include "libipa/ccm.h"
> >  #include "libipa/fc_queue.h"
> >  #include "libipa/fixedpoint.h"
> > +#include "libipa/lsc.h"
> >
> >  namespace libcamera {
> >
> > @@ -138,9 +139,7 @@ struct IPAActiveState {
> >                 double strength;
> >         } wdr;
> >
> > -       struct {
> > -               bool enabled;
> > -       } lsc;
> > +       ipa::lsc::ActiveState lsc;
>
> That will really ease IPA development.
>
> >  };
> >
> >  struct IPAFrameContext : public FrameContext {
> > @@ -213,10 +212,7 @@ struct IPAFrameContext : public FrameContext {
> >                 double gain;
> >         } wdr;
> >
> > -       struct {
> > -               bool enabled;
> > -               bool update;
> > -       } lsc;
> > +       ipa::lsc::FrameContext lsc;
> >  };
>
> This is a really massive patch and I wonder if it would be possible to
> split it out into multiple changes (also in case we have regressions). I
> know this is more work, but otoh only rkisp1 uses that. So it would be
> possible?

Everything is possible.

I guess we'll miss the next release though.

>
> Best regards,
> Stefan
>
> >
> >  struct IPAContext {
> >
> > --
> > 2.54.0
> >
Laurent Pinchart June 26, 2026, 7:51 p.m. UTC | #7
On Fri, Jun 26, 2026 at 04:26:53PM +0200, Jacopo Mondi wrote:
> On Fri, Jun 26, 2026 at 11:18:12AM +0200, Stefan Klug wrote:
> > Quoting Jacopo Mondi (2026-06-15 16:05:34)
> > > Introduce an Lsc algorithm implementation in libIPA.
> > >
> > > The LscAlgorithm class supports two algorithms: grid-based lsc and
> > > polynomial-based lsc.
> > >
> > > Both implementations have been generalized from the existing ones in the
> > > RkISP1 IPA.
> > >
> > > Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> > > ---
> > >  src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++
> > >  src/ipa/libipa/lsc.h              | 145 +++++++++++++
> > >  src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++
> > >  src/ipa/libipa/lsc_base.h         |  59 ++++++
> > >  src/ipa/libipa/lsc_grid.cpp       |  33 +++
> > >  src/ipa/libipa/lsc_grid.h         | 113 ++++++++++
> > >  src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-
> > >  src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----
> > >  src/ipa/libipa/meson.build        |   5 +
> > >  src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------
> > >  src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--
> > >  src/ipa/rkisp1/ipa_context.h      |  10 +-
> > >  12 files changed, 1126 insertions(+), 450 deletions(-)
> > >
> > > diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
> > > new file mode 100644
> > > index 000000000000..ae713a776778
> > > --- /dev/null
> > > +++ b/src/ipa/libipa/lsc.cpp
> > > @@ -0,0 +1,322 @@

[snip]

> > > +/**
> > > + * \class LscAlgorithm
> > > + * \brief libIPA lsc algorithm implementation
> > > + * \tparam T The type used to store the gain values when loaded from tuning file
> > > + * \tparam U The fixedpoint lsc engine register format
> > > + *
> > > + * Due to the optical characteristics of the lens, the light intensity received
> > > + * by the sensor is not uniform. The Lens Shading Correction algorithm applies
> > > + * multipliers to all pixels to compensate for the lens shading effect.
> > > + *
> > > + * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.
> > > + * It provides support for parsing the tuning file content and generate tables
> > > + * of per-colour temperature gains that IPA algorithms can use to program their
> > > + * lsc engine.
> > > + *
> > > + * The init() function parses the tuning file and loads the gain tables either
> > > + * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain
> > > + * tables are organized per-colour temperature with per-colour components gain
> > > + * vectors or polynomial coefficients. The colour components names are
> > > + * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc
> > > + * engine support 4 colour components (r, gr, gb, b), some only support 3 colour
> > > + * components (r, g, b). The name (and number) of the expected colour components
> > > + * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys
> > > + * field.
> > > + *
> > > + * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour
> > > + * components. The gain table has been here omitted, but the expected number
> >
> > Nit: s/here omitted/omitted here/
> 
> I think 'here omitted' is correct

"omitted here" sounds more correct.

> > > + * of entries has be equal to
> >
> > Nit: has to be
> >
> > > + * LscDescriptor::numHCells * LscDescriptor::numVCells.
> > > + *
> > > + * \code{.yaml}
> > > + * - Lsc:
> > > + *    sets:
> > > + *      - ct: 2500
> > > + *        r: [
> > > + *             .. gains table omitted..
> > > + *        ]
> > > + *        g: [
> > > + *             .. gains table omitted..
> > > + *        ]
> > > + *        b: [
> > > + *             .. gains table omitted..
> > > + *        ]
> > > + *      - ct: 6500
> > > + *        r: [
> > > + *             .. gains table omitted..
> > > + *        ]
> > > + *        g: [
> > > + *             .. gains table omitted..
> > > + *        ]
> > > + *        b: [
> > > + *             .. gains table omitted..
> > > + *        ]
> > > + * \endcode
> > > + *
> > > + * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'
> > > + * colour components:
> > > + *
> > > + * \code{.yaml}
> > > + * - Lsc:
> > > + *    type: "polynomial"
> > > + *    sets:
> > > + *      - ct: 2500
> > > + *        r:
> > > + *          cx: 0.5006571711950275
> > > + *          cy: 0.510093737499277
> > > + *          k0: 1.5393282208428813
> > > + *          k1: -1.1434559757908016
> > > + *          k2: 4.332602305814554
> > > + *          k3: 0.0
> > > + *          k4: 0.0
> > > + *        gr:
> > > + *          cx: 0.5009320529087338
> > > + *          cy: 0.511208038949085
> > > + *          k0: 1.5634738574805407
> > > + *          k1: -1.5623484259968348
> > > + *          k2: 4.846686073656501
> > > + *          k3: 0.0
> > > + *          k4: 0.0
> > > + *        gb:
> > > + *          cx: 0.5012013290343839
> > > + *          cy: 0.5128251541578288
> > > + *          k0: 1.526147944919103
> > > + *          k1: -1.4316976083689723
> > > + *          k2: 4.792604063222728
> > > + *          k3: 0.0
> > > + *          k4: 0.0
> > > + *        b:
> > > + *          cx: 0.49864139511067784
> > > + *          cy: 0.5162095081739346
> > > + *          k0: 1.0405245474038738
> > > + *          k1: 0.05618339879447103
> > > + *          k2: 1.8792813594001752
> > > + *          k3: 0.0
> > > + *          k4: 0.0
> > > + *      - ct: 6000
> > > + *        r:
> > > + *          cx: 0.5006202239353942
> > > + *          cy: 0.5099531318307661
> > > + *          k0: 1.4702946023945032
> > > + *          k1: -0.8893767547927631
> > > + *          k2: 3.920547732201387
> > > + *          k3: 0.0
> > > + *          k4: 0.0
> > > + *        gr:
> > > + *          cx: 0.500907874178317
> > > + *          cy: 0.511084916024106
> > > + *          k0: 1.5336172760559457
> > > + *          k1: -1.39964026514435
> > > + *          k2: 4.565487728954618
> > > + *          k3: 0.0
> > > + *          k4: 0.0
> > > + *        gb:
> > > + *          cx: 0.5011898608900477
> > > + *          cy: 0.5126797906745105
> > > + *          k0: 1.5013145790354843
> > > + *          k1: -1.2747407173754124
> > > + *          k2: 4.514682876897286
> > > + *          k3: 0.0
> > > + *          k4: 0.0
> > > + *        b:
> > > + *          cx: 0.4987561413116136
> > > + *          cy: 0.5159619420778772
> > > + *          k0: 1.0102986422191802
> > > + *          k1: 0.13263449763985727
> > > + *          k2: 1.686556107316064
> > > + *          k3: 0.0
> > > + *          k4: 0.0
> > > + * \endcode
> > > + *
> > > + * The lsc tables or the polynomial definition are generated at tuning time
> > > + * using an image of known resolution which needs to be specified in
> > > + * LscDescriptor::sensorSize.
> > > + *
> > > + * At LscAlgorithm::configure() time the lsc tables are re-sampled on the
> > > + * sensor's crop rectangle in use to adapt them to the configuration in use for
> > > + * a streaming session. Polynomial lsc tables support re-sampling and can be
> > > + * applied to any sensor configuration. Grid-based lsc tables cannot be
> > > + * re-sampled and the configuration as parsed from the tuning file is used for
> > > + * all sensor configurations providing best-effort results.
> > > + *
> > > + * When the IPA algorithms wants to get access to the (resampled) tables to
> > > + * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get
> > > + * an lsc table interpolated by the LscAlgorithm class for the specified colour
> > > + * temperature. If the algorithm wants to access the non-interpolated tables it
> > > + * can retrieve them using LscAlgorithm::getComponents().
> > > + */

[snip]

> > > diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
> > > new file mode 100644
> > > index 000000000000..aecae305aa9c
> > > --- /dev/null
> > > +++ b/src/ipa/libipa/lsc_base.h
> > > @@ -0,0 +1,59 @@
> > > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > > +/*
> > > + * Copyright (C) 2026 Ideas on Board Oy
> > > + *
> > > + * Base classes and types for LSC algorithms implementations
> > > + */
> > > +
> > > +#pragma once
> > > +
> > > +#include <map>
> > > +#include <string>
> > > +#include <vector>
> > > +
> > > +#include <libcamera/geometry.h>
> > > +
> > > +#include "libcamera/internal/value_node.h"
> > > +
> > > +namespace libcamera {
> > > +
> > > +namespace ipa {
> > > +
> > > +namespace lsc {
> > > +
> > > +template<typename T>
> > > +class Components : public std::map<std::string, std::vector<T>>
> > > +{
> >
> > I'm a bit uneasy here. On the one hand we are going great lengths to
> > write super optimized code striving for static allocations as much as
> > possible and now we add a string based map lookup that is run per frame.
> 
> How are 'string based lookup' and 'static memory allocation' related ?
> 
> > On the other hand I don't actually know how expensive the lookup is.
> 
> So, Component is used at init to parse the tuning file. Fine.
> 
> At configure a set of Components is re-sampled (polynomial) or
> returned as parsed from config file. Fine.
> 
> Now, the run-time.
> 
> I presume you're thinking at (using rkisp1 as an example)
> 
>  void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
> -                                     const Components &set)
> +                                     const ipa::lsc::Components<uint16_t> &set)
>  {
> -       std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
> -       std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
> -       std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
> -       std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
> +       const auto &r = set.at("r");
> +       std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
> +       const auto &gr = set.at("gr");
> +       std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
> +       const auto &gb = set.at("gb");
> +       std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
> +       const auto &b = set.at("b");
> +       std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
>  }
> 
> yes, we do a lookup on a string.
> 
> The existing implementation stores the Components vectors in a struct
> 
> 	struct Components {
> 		uint32_t ct;
> 		std::vector<uint16_t> r;
> 		std::vector<uint16_t> gr;
> 		std::vector<uint16_t> gb;
> 		std::vector<uint16_t> b;
> 	};
> 
> This patch instead stores them in a map.
> 
> Now, not an STL guru here, but I presume that if your only concern is
> the lookup, a map with 4 elements top shouldn't be that hard.
> 
> If there are implications I'm missing about storing the vectors in the
> map instead of storing them in a struct I would be glad to learn about
> that.

A map with string keys will be significantly less efficient. First the
string will be hashed, then the hash will be used in the lookup, and
finally the strings will be compared.

> > I was wondering if it is possible to template that using a static set as
> > temüplate param and a fixed size array as container but then you can't
> > configure that at init() time and end up with too much template
> > magic....no
> >
> > As a compromise, would it make sense to index the map by an enum, so
> > that we could define an enum for the possible types (it will most likely
> > ever only be g,r,b,rg,bg (possible ir in the future). That would at
> > least replace the string lookup by an int lookup, and we'd only have
> > strings at yaml parse time?

If we have an index, can't we just make it an array or vector ?

> So is the only concern really comparing strings vs comparing integers
> when looking up a key ? In the same code path that memcpys thousands
> of values to program the lsc tables ?
> 
> I feel like I'm missing something.
> 
> > > +};
> > > +
> > > +template<typename T>
> > > +class ComponentsMap : public std::map<unsigned int, Components<T>>
> > > +{
> > > +};
> > > +
> > > +} /* namespace lsc */
> > > +
> > > +struct LscDescriptor {
> > > +       std::vector<std::string> keys;
> > > +       unsigned int numHCells;
> > > +       unsigned int numVCells;
> > > +       Size sensorSize;
> > > +};
> > > +
> > > +template<typename T, typename U>
> > > +class LscImplementation
> >
> > On v1 Dan commented that U is not used in grid based. But it is used in
> > the polynomial case. What about dropping T and defining it as
> > U::QuantizedType? That would work for both cases and you get rid of one
> > parameter. (And if there is a hardware that has a different register
> > size than U::QuantizedType, the current solution would rely on luck,
> > too.)
> 
> This one is a good suggestion.
> 
> Right now the template argument T serves only for loading values from
> file in a type large enough to accommodate them
> 
> Using U::QuantizedType seems like a good idea.
> We're gonna drop it anyway an move to store and load float gains from
> the tuning file, but in the interim using Q::QuantizedType seems good
> enough.
> 
> > > +{
> > > +public:
> > > +       virtual ~LscImplementation() = default;
> > > +
> > > +       virtual int parseLscData(const ValueNode &tuningData,
> > > +                                const LscDescriptor &descriptor) = 0;
> > > +
> > > +       virtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> > > +                                                     const std::vector<double> &xPos,
> > > +                                                     const std::vector<double> &yPos) = 0;
> > > +};
> > > +
> > > +} /* namespace ipa */
> > > +
> > > +} /* namespace libcamera */

[snip]

> > > diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
> > > index f607d86c54c3..8d3f1883d6a5 100644
> > > --- a/src/ipa/libipa/lsc_polynomial.cpp
> > > +++ b/src/ipa/libipa/lsc_polynomial.cpp
> > > @@ -1,12 +1,14 @@
> > >  /* SPDX-License-Identifier: LGPL-2.1-or-later */
> > >  /*
> > > - * Copyright (C) 2024, Ideas On Board
> > > + * Copyright (C) 2026, Ideas On Board
> > >   *
> > > - * Polynomial class to represent lens shading correction
> > > + * Polynomial based lens shading correction
> > >   */
> > >
> > >  #include "lsc_polynomial.h"
> > >
> > > +#include <assert.h>
> > > +
> > >  #include <libcamera/base/log.h>
> > >
> > >  /**
> > > @@ -21,7 +23,7 @@ LOG_DEFINE_CATEGORY(LscPolynomial)
> > >  namespace ipa {
> > >
> > >  /**
> > > - * \class LscPolynomial
> > > + * \class Polynomial
> > >   * \brief Class for handling even polynomials used in lens shading correction
> > >   *
> > >   * Shading artifacts of camera lenses can be modeled using even radial
> > > @@ -31,7 +33,7 @@ namespace ipa {
> > >   */
> > >
> > >  /**
> > > - * \fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > > + * \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > >                       double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> > >                       double k4 = 0.0)
> > >   * \brief Construct a polynomial using the given coefficients
> > > @@ -45,7 +47,13 @@ namespace ipa {
> > >   */
> > >
> > >  /**
> > > - * \fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)
> > > + * \fn Polynomial::Polynomial(const Polynomial &other)
> > > + * \brief Construct a Polynomial by copy
> > > + * \param[in] other The Polynomial to copy construct from
> > > + */
> > > +
> > > +/**
> > > + * \fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)
> > >   * \brief Sample the polynomial at the given normalized pixel position
> > >   *
> > >   * This functions samples the polynomial at the given pixel position divided by
> > > @@ -55,9 +63,21 @@ namespace ipa {
> > >   * \param y y position in normalized coordinates
> > >   * \return The sampled value
> > >   */
> > > +double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
> > > +{
> > > +       double dx = x - cnx_;
> > > +       double dy = y - cny_;
> > > +       double r = sqrt(dx * dx + dy * dy);
> > > +       double res = 1.0;
> > > +
> > > +       for (unsigned int i = 0; i < coefficients_.size(); i++)
> > > +               res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> > > +
> > > +       return res;
> > > +}
> >
> > These are plain refactorings. I see that the whole libipa rework
> > is difficult to do in small patches, but I think it would make review
> > (and later history reading) easier if we'd cut out these refactorings
> > into separate preparatory patches.
> 
> I think we all knew that this work was going to be hard to review as
> it was hard to implement.
> 
> I again suggest to look at the final result more than the diff.

:-(

> I could break the patches down in small non-compilable units if it
> helps.

It will help review, if those smaller patches are self-contained and can
be reviewed on their own, even if they don't necessarily compile on
their own.

> > >
> > >  /**
> > > - * \fn LscPolynomial::getM()
> > > + * \fn Polynomial::getM()
> > >   * \brief Get the value m as described in the dng specification
> > >   *
> > >   * Returns m according to dng spec. m represents the Euclidean distance

[snip]

> > > diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
> > > index 0a256e225327..7c656d95acfb 100644
> > > --- a/src/ipa/rkisp1/algorithms/lsc.h
> > > +++ b/src/ipa/rkisp1/algorithms/lsc.h
> > > @@ -2,17 +2,23 @@
> > >  /*
> > >   * Copyright (C) 2021-2022, Ideas On Board
> > >   *
> > > - * RkISP1 Lens Shading Correction control
> > > + * RkISP1 Lens Shading Correction algorithm
> > >   */
> > >
> > >  #pragma once
> > >
> > > -#include <map>
> > > -#include <memory>
> > > +#include <vector>
> > >
> > > -#include "libipa/interpolator.h"
> > > +#include <linux/rkisp1-config.h>
> > > +
> > > +#include "libcamera/internal/value_node.h"
> > > +
> > > +#include "libipa/fixedpoint.h"
> > > +#include "libipa/lsc.h"
> > >
> > >  #include "algorithm.h"
> > > +#include "ipa_context.h"
> > > +#include "params.h"
> > >
> > >  namespace libcamera {
> > >
> > > @@ -37,39 +43,28 @@ public:
> > >                      const rkisp1_stat_buffer *stats,
> > >                      ControlList &metadata) override;
> > >
> > > -       struct Components {
> > > -               std::vector<uint16_t> r;
> > > -               std::vector<uint16_t> gr;
> > > -               std::vector<uint16_t> gb;
> > > -               std::vector<uint16_t> b;
> > > -       };
> > > -
> > > -       class ShadingDescriptor
> > > -       {
> > > -       public:
> > > -               virtual ~ShadingDescriptor() = default;
> > > -               virtual Components sampleForCrop(const Rectangle &cropRectangle,
> > > -                                                Span<const double> xSizes,
> > > -                                                Span<const double> ySizes) = 0;
> > > -       };
> > > -
> > > -       using ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;
> > > -
> > >  private:
> > > +       std::vector<double> parseSizes(const ValueNode &tuningData,
> > > +                                      const char *prop);
> > > +       std::vector<double> sizesToPositions(Span<const double> sizes);
> > > +
> > >         void setParameters(rkisp1_cif_isp_lsc_config &config);
> > > -       void copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);
> > > +       void copyTable(rkisp1_cif_isp_lsc_config &config,
> > > +                      const ipa::lsc::Components<uint16_t> &set0);
> > >
> > > -       ShadingDescriptorMap shadingDescriptors_;
> > > -       ipa::Interpolator<Components> sets_;
> > >         std::vector<double> xSize_;
> > >         std::vector<double> ySize_;
> > >         uint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > >         uint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > >         uint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > >         uint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > +       std::vector<double> xPos_;
> > > +       std::vector<double> yPos_;
> > >
> > >         unsigned int lastAppliedCt_;
> > >         unsigned int lastAppliedQuantizedCt_;
> > > +
> > > +       LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;
> >
> > I don't really like specifying the fixed format at compile time, as we
> > will have a hard time to implement support for different formats at
> > runtime like Mali supports. But I also don't see an easy way out of
> > that.
> >
> > >  };
> > >
> > >  } /* namespace ipa::rkisp1::algorithms */
> > > diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
> > > index cd97e10bcf2b..005f4102b4f6 100644
> > > --- a/src/ipa/rkisp1/ipa_context.h
> > > +++ b/src/ipa/rkisp1/ipa_context.h
> > > @@ -30,6 +30,7 @@
> > >  #include "libipa/ccm.h"
> > >  #include "libipa/fc_queue.h"
> > >  #include "libipa/fixedpoint.h"
> > > +#include "libipa/lsc.h"
> > >
> > >  namespace libcamera {
> > >
> > > @@ -138,9 +139,7 @@ struct IPAActiveState {
> > >                 double strength;
> > >         } wdr;
> > >
> > > -       struct {
> > > -               bool enabled;
> > > -       } lsc;
> > > +       ipa::lsc::ActiveState lsc;
> >
> > That will really ease IPA development.
> >
> > >  };
> > >
> > >  struct IPAFrameContext : public FrameContext {
> > > @@ -213,10 +212,7 @@ struct IPAFrameContext : public FrameContext {
> > >                 double gain;
> > >         } wdr;
> > >
> > > -       struct {
> > > -               bool enabled;
> > > -               bool update;
> > > -       } lsc;
> > > +       ipa::lsc::FrameContext lsc;
> > >  };
> >
> > This is a really massive patch and I wonder if it would be possible to
> > split it out into multiple changes (also in case we have regressions). I
> > know this is more work, but otoh only rkisp1 uses that. So it would be
> > possible?
> 
> Everything is possible.
> 
> I guess we'll miss the next release though.

Don't worry about releases. They are just numbers. Let's move forward
with this work, give it review priority, and merge it when it will be
ready.

> > >  struct IPAContext {
Jacopo Mondi June 27, 2026, 9:24 a.m. UTC | #8
Hi Laurent

On Fri, Jun 26, 2026 at 10:51:51PM +0300, Laurent Pinchart wrote:
> On Fri, Jun 26, 2026 at 04:26:53PM +0200, Jacopo Mondi wrote:
> > On Fri, Jun 26, 2026 at 11:18:12AM +0200, Stefan Klug wrote:
> > > Quoting Jacopo Mondi (2026-06-15 16:05:34)
> > > > Introduce an Lsc algorithm implementation in libIPA.
> > > >
> > > > The LscAlgorithm class supports two algorithms: grid-based lsc and
> > > > polynomial-based lsc.
> > > >
> > > > Both implementations have been generalized from the existing ones in the
> > > > RkISP1 IPA.
> > > >
> > > > Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> > > > ---
> > > >  src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++
> > > >  src/ipa/libipa/lsc.h              | 145 +++++++++++++
> > > >  src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++
> > > >  src/ipa/libipa/lsc_base.h         |  59 ++++++
> > > >  src/ipa/libipa/lsc_grid.cpp       |  33 +++
> > > >  src/ipa/libipa/lsc_grid.h         | 113 ++++++++++
> > > >  src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-
> > > >  src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----
> > > >  src/ipa/libipa/meson.build        |   5 +
> > > >  src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------
> > > >  src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--
> > > >  src/ipa/rkisp1/ipa_context.h      |  10 +-
> > > >  12 files changed, 1126 insertions(+), 450 deletions(-)
> > > >
> > > > diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
> > > > new file mode 100644
> > > > index 000000000000..ae713a776778
> > > > --- /dev/null
> > > > +++ b/src/ipa/libipa/lsc.cpp
> > > > @@ -0,0 +1,322 @@
>
> [snip]
>
> > > > +/**
> > > > + * \class LscAlgorithm
> > > > + * \brief libIPA lsc algorithm implementation
> > > > + * \tparam T The type used to store the gain values when loaded from tuning file
> > > > + * \tparam U The fixedpoint lsc engine register format
> > > > + *
> > > > + * Due to the optical characteristics of the lens, the light intensity received
> > > > + * by the sensor is not uniform. The Lens Shading Correction algorithm applies
> > > > + * multipliers to all pixels to compensate for the lens shading effect.
> > > > + *
> > > > + * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.
> > > > + * It provides support for parsing the tuning file content and generate tables
> > > > + * of per-colour temperature gains that IPA algorithms can use to program their
> > > > + * lsc engine.
> > > > + *
> > > > + * The init() function parses the tuning file and loads the gain tables either
> > > > + * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain
> > > > + * tables are organized per-colour temperature with per-colour components gain
> > > > + * vectors or polynomial coefficients. The colour components names are
> > > > + * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc
> > > > + * engine support 4 colour components (r, gr, gb, b), some only support 3 colour
> > > > + * components (r, g, b). The name (and number) of the expected colour components
> > > > + * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys
> > > > + * field.
> > > > + *
> > > > + * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour
> > > > + * components. The gain table has been here omitted, but the expected number
> > >
> > > Nit: s/here omitted/omitted here/
> >
> > I think 'here omitted' is correct
>
> "omitted here" sounds more correct.
>
> > > > + * of entries has be equal to
> > >
> > > Nit: has to be
> > >
> > > > + * LscDescriptor::numHCells * LscDescriptor::numVCells.
> > > > + *
> > > > + * \code{.yaml}
> > > > + * - Lsc:
> > > > + *    sets:
> > > > + *      - ct: 2500
> > > > + *        r: [
> > > > + *             .. gains table omitted..
> > > > + *        ]
> > > > + *        g: [
> > > > + *             .. gains table omitted..
> > > > + *        ]
> > > > + *        b: [
> > > > + *             .. gains table omitted..
> > > > + *        ]
> > > > + *      - ct: 6500
> > > > + *        r: [
> > > > + *             .. gains table omitted..
> > > > + *        ]
> > > > + *        g: [
> > > > + *             .. gains table omitted..
> > > > + *        ]
> > > > + *        b: [
> > > > + *             .. gains table omitted..
> > > > + *        ]
> > > > + * \endcode
> > > > + *
> > > > + * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'
> > > > + * colour components:
> > > > + *
> > > > + * \code{.yaml}
> > > > + * - Lsc:
> > > > + *    type: "polynomial"
> > > > + *    sets:
> > > > + *      - ct: 2500
> > > > + *        r:
> > > > + *          cx: 0.5006571711950275
> > > > + *          cy: 0.510093737499277
> > > > + *          k0: 1.5393282208428813
> > > > + *          k1: -1.1434559757908016
> > > > + *          k2: 4.332602305814554
> > > > + *          k3: 0.0
> > > > + *          k4: 0.0
> > > > + *        gr:
> > > > + *          cx: 0.5009320529087338
> > > > + *          cy: 0.511208038949085
> > > > + *          k0: 1.5634738574805407
> > > > + *          k1: -1.5623484259968348
> > > > + *          k2: 4.846686073656501
> > > > + *          k3: 0.0
> > > > + *          k4: 0.0
> > > > + *        gb:
> > > > + *          cx: 0.5012013290343839
> > > > + *          cy: 0.5128251541578288
> > > > + *          k0: 1.526147944919103
> > > > + *          k1: -1.4316976083689723
> > > > + *          k2: 4.792604063222728
> > > > + *          k3: 0.0
> > > > + *          k4: 0.0
> > > > + *        b:
> > > > + *          cx: 0.49864139511067784
> > > > + *          cy: 0.5162095081739346
> > > > + *          k0: 1.0405245474038738
> > > > + *          k1: 0.05618339879447103
> > > > + *          k2: 1.8792813594001752
> > > > + *          k3: 0.0
> > > > + *          k4: 0.0
> > > > + *      - ct: 6000
> > > > + *        r:
> > > > + *          cx: 0.5006202239353942
> > > > + *          cy: 0.5099531318307661
> > > > + *          k0: 1.4702946023945032
> > > > + *          k1: -0.8893767547927631
> > > > + *          k2: 3.920547732201387
> > > > + *          k3: 0.0
> > > > + *          k4: 0.0
> > > > + *        gr:
> > > > + *          cx: 0.500907874178317
> > > > + *          cy: 0.511084916024106
> > > > + *          k0: 1.5336172760559457
> > > > + *          k1: -1.39964026514435
> > > > + *          k2: 4.565487728954618
> > > > + *          k3: 0.0
> > > > + *          k4: 0.0
> > > > + *        gb:
> > > > + *          cx: 0.5011898608900477
> > > > + *          cy: 0.5126797906745105
> > > > + *          k0: 1.5013145790354843
> > > > + *          k1: -1.2747407173754124
> > > > + *          k2: 4.514682876897286
> > > > + *          k3: 0.0
> > > > + *          k4: 0.0
> > > > + *        b:
> > > > + *          cx: 0.4987561413116136
> > > > + *          cy: 0.5159619420778772
> > > > + *          k0: 1.0102986422191802
> > > > + *          k1: 0.13263449763985727
> > > > + *          k2: 1.686556107316064
> > > > + *          k3: 0.0
> > > > + *          k4: 0.0
> > > > + * \endcode
> > > > + *
> > > > + * The lsc tables or the polynomial definition are generated at tuning time
> > > > + * using an image of known resolution which needs to be specified in
> > > > + * LscDescriptor::sensorSize.
> > > > + *
> > > > + * At LscAlgorithm::configure() time the lsc tables are re-sampled on the
> > > > + * sensor's crop rectangle in use to adapt them to the configuration in use for
> > > > + * a streaming session. Polynomial lsc tables support re-sampling and can be
> > > > + * applied to any sensor configuration. Grid-based lsc tables cannot be
> > > > + * re-sampled and the configuration as parsed from the tuning file is used for
> > > > + * all sensor configurations providing best-effort results.
> > > > + *
> > > > + * When the IPA algorithms wants to get access to the (resampled) tables to
> > > > + * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get
> > > > + * an lsc table interpolated by the LscAlgorithm class for the specified colour
> > > > + * temperature. If the algorithm wants to access the non-interpolated tables it
> > > > + * can retrieve them using LscAlgorithm::getComponents().
> > > > + */
>
> [snip]
>
> > > > diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
> > > > new file mode 100644
> > > > index 000000000000..aecae305aa9c
> > > > --- /dev/null
> > > > +++ b/src/ipa/libipa/lsc_base.h
> > > > @@ -0,0 +1,59 @@
> > > > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > > > +/*
> > > > + * Copyright (C) 2026 Ideas on Board Oy
> > > > + *
> > > > + * Base classes and types for LSC algorithms implementations
> > > > + */
> > > > +
> > > > +#pragma once
> > > > +
> > > > +#include <map>
> > > > +#include <string>
> > > > +#include <vector>
> > > > +
> > > > +#include <libcamera/geometry.h>
> > > > +
> > > > +#include "libcamera/internal/value_node.h"
> > > > +
> > > > +namespace libcamera {
> > > > +
> > > > +namespace ipa {
> > > > +
> > > > +namespace lsc {
> > > > +
> > > > +template<typename T>
> > > > +class Components : public std::map<std::string, std::vector<T>>
> > > > +{
> > >
> > > I'm a bit uneasy here. On the one hand we are going great lengths to
> > > write super optimized code striving for static allocations as much as
> > > possible and now we add a string based map lookup that is run per frame.
> >
> > How are 'string based lookup' and 'static memory allocation' related ?
> >
> > > On the other hand I don't actually know how expensive the lookup is.
> >
> > So, Component is used at init to parse the tuning file. Fine.
> >
> > At configure a set of Components is re-sampled (polynomial) or
> > returned as parsed from config file. Fine.
> >
> > Now, the run-time.
> >
> > I presume you're thinking at (using rkisp1 as an example)
> >
> >  void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
> > -                                     const Components &set)
> > +                                     const ipa::lsc::Components<uint16_t> &set)
> >  {
> > -       std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
> > -       std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
> > -       std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
> > -       std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
> > +       const auto &r = set.at("r");
> > +       std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
> > +       const auto &gr = set.at("gr");
> > +       std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
> > +       const auto &gb = set.at("gb");
> > +       std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
> > +       const auto &b = set.at("b");
> > +       std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
> >  }
> >
> > yes, we do a lookup on a string.
> >
> > The existing implementation stores the Components vectors in a struct
> >
> > 	struct Components {
> > 		uint32_t ct;
> > 		std::vector<uint16_t> r;
> > 		std::vector<uint16_t> gr;
> > 		std::vector<uint16_t> gb;
> > 		std::vector<uint16_t> b;
> > 	};
> >
> > This patch instead stores them in a map.
> >
> > Now, not an STL guru here, but I presume that if your only concern is
> > the lookup, a map with 4 elements top shouldn't be that hard.
> >
> > If there are implications I'm missing about storing the vectors in the
> > map instead of storing them in a struct I would be glad to learn about
> > that.
>
> A map with string keys will be significantly less efficient. First the
> string will be hashed, then the hash will be used in the lookup, and

Are you sure hashing happen for std::map too or is it just for
std::unordered_map ?

As far as I know, std::map sorts the keys by comparing them and
the default Compare function is std::less<> which for std::string calls
std::string::operator< which has a complexity linear in the string
length (1 or 2 chars). Then the sorted key lookup itself should be
O(log n), which for a map of up to two elements is nothing.

If using an unordered_map<> then hasing of the key (which [1] doesn't
report the complexity of) will take place, then the lookup should
happen in linear time.

[1] https://en.cppreference.com/cpp/string/basic_string/hash
> finally the strings will be compared.
>

When the LSC tables have to be updated because of a significant change
in colour temperature we interpolate vectors of thousands of elements
for the new colour temperature and then memcpy them to the parameters buffer.

Before reworking anything here I would like to see a comparison of these
costs to the above described complexity of using a map of 4 elements
indexed by a string of 1 or 2 characters.

And no, we're not:

> > > going great lengths to
> > > write super optimized code striving for static allocations as much as
> > > possible

Both when loading from yaml file and when re-sampling the polynomial
the destination vectors are dynamically allocatated. This is good, it
avoids relocations, but it's certainly not "static allocation as much
as possible" and I don't think anyone complained.

> > > I was wondering if it is possible to template that using a static set as
> > > temüplate param and a fixed size array as container but then you can't
> > > configure that at init() time and end up with too much template
> > > magic....no
> > >
> > > As a compromise, would it make sense to index the map by an enum, so
> > > that we could define an enum for the possible types (it will most likely
> > > ever only be g,r,b,rg,bg (possible ir in the future). That would at
> > > least replace the string lookup by an int lookup, and we'd only have
> > > strings at yaml parse time?
>
> If we have an index, can't we just make it an array or vector ?
>

Each IPA has different indexes that reflect the number of tables the
hadware supprots ('r' 'g' 'b' for mali, 'r' 'gr' 'gb' 'b' for rkisp1
etc). The current implementation allows the IPA to specify which keys
to use to parse the tables (or polynomials) from the tuning file and
then use the same keys for tables lookup when LSC have to be updated.

This is of course the most flexible solution. We can provide a less
flexible interface to IPAs and assume up to 4 vectors are supported
and let IPA map the array entries to the colour component.

Again, this seems largely unecessary to me considering the above
described cost of using a map is justified by a nicer API.

> > So is the only concern really comparing strings vs comparing integers
> > when looking up a key ? In the same code path that memcpys thousands
> > of values to program the lsc tables ?
> >
> > I feel like I'm missing something.
> >
> > > > +};
> > > > +
> > > > +template<typename T>
> > > > +class ComponentsMap : public std::map<unsigned int, Components<T>>
> > > > +{
> > > > +};
> > > > +
> > > > +} /* namespace lsc */
> > > > +
> > > > +struct LscDescriptor {
> > > > +       std::vector<std::string> keys;
> > > > +       unsigned int numHCells;
> > > > +       unsigned int numVCells;
> > > > +       Size sensorSize;
> > > > +};
> > > > +
> > > > +template<typename T, typename U>
> > > > +class LscImplementation
> > >
> > > On v1 Dan commented that U is not used in grid based. But it is used in
> > > the polynomial case. What about dropping T and defining it as
> > > U::QuantizedType? That would work for both cases and you get rid of one
> > > parameter. (And if there is a hardware that has a different register
> > > size than U::QuantizedType, the current solution would rely on luck,
> > > too.)
> >
> > This one is a good suggestion.
> >
> > Right now the template argument T serves only for loading values from
> > file in a type large enough to accommodate them
> >
> > Using U::QuantizedType seems like a good idea.
> > We're gonna drop it anyway an move to store and load float gains from
> > the tuning file, but in the interim using Q::QuantizedType seems good
> > enough.
> >
> > > > +{
> > > > +public:
> > > > +       virtual ~LscImplementation() = default;
> > > > +
> > > > +       virtual int parseLscData(const ValueNode &tuningData,
> > > > +                                const LscDescriptor &descriptor) = 0;
> > > > +
> > > > +       virtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> > > > +                                                     const std::vector<double> &xPos,
> > > > +                                                     const std::vector<double> &yPos) = 0;
> > > > +};
> > > > +
> > > > +} /* namespace ipa */
> > > > +
> > > > +} /* namespace libcamera */
>
> [snip]
>
> > > > diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
> > > > index f607d86c54c3..8d3f1883d6a5 100644
> > > > --- a/src/ipa/libipa/lsc_polynomial.cpp
> > > > +++ b/src/ipa/libipa/lsc_polynomial.cpp
> > > > @@ -1,12 +1,14 @@
> > > >  /* SPDX-License-Identifier: LGPL-2.1-or-later */
> > > >  /*
> > > > - * Copyright (C) 2024, Ideas On Board
> > > > + * Copyright (C) 2026, Ideas On Board
> > > >   *
> > > > - * Polynomial class to represent lens shading correction
> > > > + * Polynomial based lens shading correction
> > > >   */
> > > >
> > > >  #include "lsc_polynomial.h"
> > > >
> > > > +#include <assert.h>
> > > > +
> > > >  #include <libcamera/base/log.h>
> > > >
> > > >  /**
> > > > @@ -21,7 +23,7 @@ LOG_DEFINE_CATEGORY(LscPolynomial)
> > > >  namespace ipa {
> > > >
> > > >  /**
> > > > - * \class LscPolynomial
> > > > + * \class Polynomial
> > > >   * \brief Class for handling even polynomials used in lens shading correction
> > > >   *
> > > >   * Shading artifacts of camera lenses can be modeled using even radial
> > > > @@ -31,7 +33,7 @@ namespace ipa {
> > > >   */
> > > >
> > > >  /**
> > > > - * \fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > > > + * \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > > >                       double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> > > >                       double k4 = 0.0)
> > > >   * \brief Construct a polynomial using the given coefficients
> > > > @@ -45,7 +47,13 @@ namespace ipa {
> > > >   */
> > > >
> > > >  /**
> > > > - * \fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)
> > > > + * \fn Polynomial::Polynomial(const Polynomial &other)
> > > > + * \brief Construct a Polynomial by copy
> > > > + * \param[in] other The Polynomial to copy construct from
> > > > + */
> > > > +
> > > > +/**
> > > > + * \fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)
> > > >   * \brief Sample the polynomial at the given normalized pixel position
> > > >   *
> > > >   * This functions samples the polynomial at the given pixel position divided by
> > > > @@ -55,9 +63,21 @@ namespace ipa {
> > > >   * \param y y position in normalized coordinates
> > > >   * \return The sampled value
> > > >   */
> > > > +double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
> > > > +{
> > > > +       double dx = x - cnx_;
> > > > +       double dy = y - cny_;
> > > > +       double r = sqrt(dx * dx + dy * dy);
> > > > +       double res = 1.0;
> > > > +
> > > > +       for (unsigned int i = 0; i < coefficients_.size(); i++)
> > > > +               res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> > > > +
> > > > +       return res;
> > > > +}
> > >
> > > These are plain refactorings. I see that the whole libipa rework
> > > is difficult to do in small patches, but I think it would make review
> > > (and later history reading) easier if we'd cut out these refactorings
> > > into separate preparatory patches.
> >
> > I think we all knew that this work was going to be hard to review as
> > it was hard to implement.
> >
> > I again suggest to look at the final result more than the diff.
>
> :-(
>
> > I could break the patches down in small non-compilable units if it
> > helps.
>
> It will help review, if those smaller patches are self-contained and can
> be reviewed on their own, even if they don't necessarily compile on
> their own.
>
> > > >
> > > >  /**
> > > > - * \fn LscPolynomial::getM()
> > > > + * \fn Polynomial::getM()
> > > >   * \brief Get the value m as described in the dng specification
> > > >   *
> > > >   * Returns m according to dng spec. m represents the Euclidean distance
>
> [snip]
>
> > > > diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
> > > > index 0a256e225327..7c656d95acfb 100644
> > > > --- a/src/ipa/rkisp1/algorithms/lsc.h
> > > > +++ b/src/ipa/rkisp1/algorithms/lsc.h
> > > > @@ -2,17 +2,23 @@
> > > >  /*
> > > >   * Copyright (C) 2021-2022, Ideas On Board
> > > >   *
> > > > - * RkISP1 Lens Shading Correction control
> > > > + * RkISP1 Lens Shading Correction algorithm
> > > >   */
> > > >
> > > >  #pragma once
> > > >
> > > > -#include <map>
> > > > -#include <memory>
> > > > +#include <vector>
> > > >
> > > > -#include "libipa/interpolator.h"
> > > > +#include <linux/rkisp1-config.h>
> > > > +
> > > > +#include "libcamera/internal/value_node.h"
> > > > +
> > > > +#include "libipa/fixedpoint.h"
> > > > +#include "libipa/lsc.h"
> > > >
> > > >  #include "algorithm.h"
> > > > +#include "ipa_context.h"
> > > > +#include "params.h"
> > > >
> > > >  namespace libcamera {
> > > >
> > > > @@ -37,39 +43,28 @@ public:
> > > >                      const rkisp1_stat_buffer *stats,
> > > >                      ControlList &metadata) override;
> > > >
> > > > -       struct Components {
> > > > -               std::vector<uint16_t> r;
> > > > -               std::vector<uint16_t> gr;
> > > > -               std::vector<uint16_t> gb;
> > > > -               std::vector<uint16_t> b;
> > > > -       };
> > > > -
> > > > -       class ShadingDescriptor
> > > > -       {
> > > > -       public:
> > > > -               virtual ~ShadingDescriptor() = default;
> > > > -               virtual Components sampleForCrop(const Rectangle &cropRectangle,
> > > > -                                                Span<const double> xSizes,
> > > > -                                                Span<const double> ySizes) = 0;
> > > > -       };
> > > > -
> > > > -       using ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;
> > > > -
> > > >  private:
> > > > +       std::vector<double> parseSizes(const ValueNode &tuningData,
> > > > +                                      const char *prop);
> > > > +       std::vector<double> sizesToPositions(Span<const double> sizes);
> > > > +
> > > >         void setParameters(rkisp1_cif_isp_lsc_config &config);
> > > > -       void copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);
> > > > +       void copyTable(rkisp1_cif_isp_lsc_config &config,
> > > > +                      const ipa::lsc::Components<uint16_t> &set0);
> > > >
> > > > -       ShadingDescriptorMap shadingDescriptors_;
> > > > -       ipa::Interpolator<Components> sets_;
> > > >         std::vector<double> xSize_;
> > > >         std::vector<double> ySize_;
> > > >         uint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > >         uint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > >         uint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > >         uint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > +       std::vector<double> xPos_;
> > > > +       std::vector<double> yPos_;
> > > >
> > > >         unsigned int lastAppliedCt_;
> > > >         unsigned int lastAppliedQuantizedCt_;
> > > > +
> > > > +       LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;
> > >
> > > I don't really like specifying the fixed format at compile time, as we
> > > will have a hard time to implement support for different formats at
> > > runtime like Mali supports. But I also don't see an easy way out of
> > > that.
> > >
> > > >  };
> > > >
> > > >  } /* namespace ipa::rkisp1::algorithms */
> > > > diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
> > > > index cd97e10bcf2b..005f4102b4f6 100644
> > > > --- a/src/ipa/rkisp1/ipa_context.h
> > > > +++ b/src/ipa/rkisp1/ipa_context.h
> > > > @@ -30,6 +30,7 @@
> > > >  #include "libipa/ccm.h"
> > > >  #include "libipa/fc_queue.h"
> > > >  #include "libipa/fixedpoint.h"
> > > > +#include "libipa/lsc.h"
> > > >
> > > >  namespace libcamera {
> > > >
> > > > @@ -138,9 +139,7 @@ struct IPAActiveState {
> > > >                 double strength;
> > > >         } wdr;
> > > >
> > > > -       struct {
> > > > -               bool enabled;
> > > > -       } lsc;
> > > > +       ipa::lsc::ActiveState lsc;
> > >
> > > That will really ease IPA development.
> > >
> > > >  };
> > > >
> > > >  struct IPAFrameContext : public FrameContext {
> > > > @@ -213,10 +212,7 @@ struct IPAFrameContext : public FrameContext {
> > > >                 double gain;
> > > >         } wdr;
> > > >
> > > > -       struct {
> > > > -               bool enabled;
> > > > -               bool update;
> > > > -       } lsc;
> > > > +       ipa::lsc::FrameContext lsc;
> > > >  };
> > >
> > > This is a really massive patch and I wonder if it would be possible to
> > > split it out into multiple changes (also in case we have regressions). I
> > > know this is more work, but otoh only rkisp1 uses that. So it would be
> > > possible?
> >
> > Everything is possible.
> >
> > I guess we'll miss the next release though.
>
> Don't worry about releases. They are just numbers. Let's move forward
> with this work, give it review priority, and merge it when it will be
> ready.
>
> > > >  struct IPAContext {
>
> --
> Regards,
>
> Laurent Pinchart
Stefan Klug June 29, 2026, 8:04 a.m. UTC | #9
Hi Jacopo,

Quoting Jacopo Mondi (2026-06-27 11:24:21)
> Hi Laurent
> 
> On Fri, Jun 26, 2026 at 10:51:51PM +0300, Laurent Pinchart wrote:
> > On Fri, Jun 26, 2026 at 04:26:53PM +0200, Jacopo Mondi wrote:
> > > On Fri, Jun 26, 2026 at 11:18:12AM +0200, Stefan Klug wrote:
> > > > Quoting Jacopo Mondi (2026-06-15 16:05:34)
> > > > > Introduce an Lsc algorithm implementation in libIPA.
> > > > >
> > > > > The LscAlgorithm class supports two algorithms: grid-based lsc and
> > > > > polynomial-based lsc.
> > > > >
> > > > > Both implementations have been generalized from the existing ones in the
> > > > > RkISP1 IPA.
> > > > >
> > > > > Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> > > > > ---
> > > > >  src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++
> > > > >  src/ipa/libipa/lsc.h              | 145 +++++++++++++
> > > > >  src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++
> > > > >  src/ipa/libipa/lsc_base.h         |  59 ++++++
> > > > >  src/ipa/libipa/lsc_grid.cpp       |  33 +++
> > > > >  src/ipa/libipa/lsc_grid.h         | 113 ++++++++++
> > > > >  src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-
> > > > >  src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----
> > > > >  src/ipa/libipa/meson.build        |   5 +
> > > > >  src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------
> > > > >  src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--
> > > > >  src/ipa/rkisp1/ipa_context.h      |  10 +-
> > > > >  12 files changed, 1126 insertions(+), 450 deletions(-)
> > > > >
> > > > > diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
> > > > > new file mode 100644
> > > > > index 000000000000..ae713a776778
> > > > > --- /dev/null
> > > > > +++ b/src/ipa/libipa/lsc.cpp
> > > > > @@ -0,0 +1,322 @@
> >
> > [snip]
> >
> > > > > +/**
> > > > > + * \class LscAlgorithm
> > > > > + * \brief libIPA lsc algorithm implementation
> > > > > + * \tparam T The type used to store the gain values when loaded from tuning file
> > > > > + * \tparam U The fixedpoint lsc engine register format
> > > > > + *
> > > > > + * Due to the optical characteristics of the lens, the light intensity received
> > > > > + * by the sensor is not uniform. The Lens Shading Correction algorithm applies
> > > > > + * multipliers to all pixels to compensate for the lens shading effect.
> > > > > + *
> > > > > + * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.
> > > > > + * It provides support for parsing the tuning file content and generate tables
> > > > > + * of per-colour temperature gains that IPA algorithms can use to program their
> > > > > + * lsc engine.
> > > > > + *
> > > > > + * The init() function parses the tuning file and loads the gain tables either
> > > > > + * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain
> > > > > + * tables are organized per-colour temperature with per-colour components gain
> > > > > + * vectors or polynomial coefficients. The colour components names are
> > > > > + * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc
> > > > > + * engine support 4 colour components (r, gr, gb, b), some only support 3 colour
> > > > > + * components (r, g, b). The name (and number) of the expected colour components
> > > > > + * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys
> > > > > + * field.
> > > > > + *
> > > > > + * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour
> > > > > + * components. The gain table has been here omitted, but the expected number
> > > >
> > > > Nit: s/here omitted/omitted here/
> > >
> > > I think 'here omitted' is correct
> >
> > "omitted here" sounds more correct.
> >
> > > > > + * of entries has be equal to
> > > >
> > > > Nit: has to be
> > > >
> > > > > + * LscDescriptor::numHCells * LscDescriptor::numVCells.
> > > > > + *
> > > > > + * \code{.yaml}
> > > > > + * - Lsc:
> > > > > + *    sets:
> > > > > + *      - ct: 2500
> > > > > + *        r: [
> > > > > + *             .. gains table omitted..
> > > > > + *        ]
> > > > > + *        g: [
> > > > > + *             .. gains table omitted..
> > > > > + *        ]
> > > > > + *        b: [
> > > > > + *             .. gains table omitted..
> > > > > + *        ]
> > > > > + *      - ct: 6500
> > > > > + *        r: [
> > > > > + *             .. gains table omitted..
> > > > > + *        ]
> > > > > + *        g: [
> > > > > + *             .. gains table omitted..
> > > > > + *        ]
> > > > > + *        b: [
> > > > > + *             .. gains table omitted..
> > > > > + *        ]
> > > > > + * \endcode
> > > > > + *
> > > > > + * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'
> > > > > + * colour components:
> > > > > + *
> > > > > + * \code{.yaml}
> > > > > + * - Lsc:
> > > > > + *    type: "polynomial"
> > > > > + *    sets:
> > > > > + *      - ct: 2500
> > > > > + *        r:
> > > > > + *          cx: 0.5006571711950275
> > > > > + *          cy: 0.510093737499277
> > > > > + *          k0: 1.5393282208428813
> > > > > + *          k1: -1.1434559757908016
> > > > > + *          k2: 4.332602305814554
> > > > > + *          k3: 0.0
> > > > > + *          k4: 0.0
> > > > > + *        gr:
> > > > > + *          cx: 0.5009320529087338
> > > > > + *          cy: 0.511208038949085
> > > > > + *          k0: 1.5634738574805407
> > > > > + *          k1: -1.5623484259968348
> > > > > + *          k2: 4.846686073656501
> > > > > + *          k3: 0.0
> > > > > + *          k4: 0.0
> > > > > + *        gb:
> > > > > + *          cx: 0.5012013290343839
> > > > > + *          cy: 0.5128251541578288
> > > > > + *          k0: 1.526147944919103
> > > > > + *          k1: -1.4316976083689723
> > > > > + *          k2: 4.792604063222728
> > > > > + *          k3: 0.0
> > > > > + *          k4: 0.0
> > > > > + *        b:
> > > > > + *          cx: 0.49864139511067784
> > > > > + *          cy: 0.5162095081739346
> > > > > + *          k0: 1.0405245474038738
> > > > > + *          k1: 0.05618339879447103
> > > > > + *          k2: 1.8792813594001752
> > > > > + *          k3: 0.0
> > > > > + *          k4: 0.0
> > > > > + *      - ct: 6000
> > > > > + *        r:
> > > > > + *          cx: 0.5006202239353942
> > > > > + *          cy: 0.5099531318307661
> > > > > + *          k0: 1.4702946023945032
> > > > > + *          k1: -0.8893767547927631
> > > > > + *          k2: 3.920547732201387
> > > > > + *          k3: 0.0
> > > > > + *          k4: 0.0
> > > > > + *        gr:
> > > > > + *          cx: 0.500907874178317
> > > > > + *          cy: 0.511084916024106
> > > > > + *          k0: 1.5336172760559457
> > > > > + *          k1: -1.39964026514435
> > > > > + *          k2: 4.565487728954618
> > > > > + *          k3: 0.0
> > > > > + *          k4: 0.0
> > > > > + *        gb:
> > > > > + *          cx: 0.5011898608900477
> > > > > + *          cy: 0.5126797906745105
> > > > > + *          k0: 1.5013145790354843
> > > > > + *          k1: -1.2747407173754124
> > > > > + *          k2: 4.514682876897286
> > > > > + *          k3: 0.0
> > > > > + *          k4: 0.0
> > > > > + *        b:
> > > > > + *          cx: 0.4987561413116136
> > > > > + *          cy: 0.5159619420778772
> > > > > + *          k0: 1.0102986422191802
> > > > > + *          k1: 0.13263449763985727
> > > > > + *          k2: 1.686556107316064
> > > > > + *          k3: 0.0
> > > > > + *          k4: 0.0
> > > > > + * \endcode
> > > > > + *
> > > > > + * The lsc tables or the polynomial definition are generated at tuning time
> > > > > + * using an image of known resolution which needs to be specified in
> > > > > + * LscDescriptor::sensorSize.
> > > > > + *
> > > > > + * At LscAlgorithm::configure() time the lsc tables are re-sampled on the
> > > > > + * sensor's crop rectangle in use to adapt them to the configuration in use for
> > > > > + * a streaming session. Polynomial lsc tables support re-sampling and can be
> > > > > + * applied to any sensor configuration. Grid-based lsc tables cannot be
> > > > > + * re-sampled and the configuration as parsed from the tuning file is used for
> > > > > + * all sensor configurations providing best-effort results.
> > > > > + *
> > > > > + * When the IPA algorithms wants to get access to the (resampled) tables to
> > > > > + * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get
> > > > > + * an lsc table interpolated by the LscAlgorithm class for the specified colour
> > > > > + * temperature. If the algorithm wants to access the non-interpolated tables it
> > > > > + * can retrieve them using LscAlgorithm::getComponents().
> > > > > + */
> >
> > [snip]
> >
> > > > > diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
> > > > > new file mode 100644
> > > > > index 000000000000..aecae305aa9c
> > > > > --- /dev/null
> > > > > +++ b/src/ipa/libipa/lsc_base.h
> > > > > @@ -0,0 +1,59 @@
> > > > > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > > > > +/*
> > > > > + * Copyright (C) 2026 Ideas on Board Oy
> > > > > + *
> > > > > + * Base classes and types for LSC algorithms implementations
> > > > > + */
> > > > > +
> > > > > +#pragma once
> > > > > +
> > > > > +#include <map>
> > > > > +#include <string>
> > > > > +#include <vector>
> > > > > +
> > > > > +#include <libcamera/geometry.h>
> > > > > +
> > > > > +#include "libcamera/internal/value_node.h"
> > > > > +
> > > > > +namespace libcamera {
> > > > > +
> > > > > +namespace ipa {
> > > > > +
> > > > > +namespace lsc {
> > > > > +
> > > > > +template<typename T>
> > > > > +class Components : public std::map<std::string, std::vector<T>>
> > > > > +{
> > > >
> > > > I'm a bit uneasy here. On the one hand we are going great lengths to
> > > > write super optimized code striving for static allocations as much as
> > > > possible and now we add a string based map lookup that is run per frame.
> > >
> > > How are 'string based lookup' and 'static memory allocation' related ?
> > >
> > > > On the other hand I don't actually know how expensive the lookup is.
> > >
> > > So, Component is used at init to parse the tuning file. Fine.
> > >
> > > At configure a set of Components is re-sampled (polynomial) or
> > > returned as parsed from config file. Fine.
> > >
> > > Now, the run-time.
> > >
> > > I presume you're thinking at (using rkisp1 as an example)
> > >
> > >  void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
> > > -                                     const Components &set)
> > > +                                     const ipa::lsc::Components<uint16_t> &set)
> > >  {
> > > -       std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
> > > -       std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
> > > -       std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
> > > -       std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
> > > +       const auto &r = set.at("r");
> > > +       std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
> > > +       const auto &gr = set.at("gr");
> > > +       std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
> > > +       const auto &gb = set.at("gb");
> > > +       std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
> > > +       const auto &b = set.at("b");
> > > +       std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
> > >  }
> > >
> > > yes, we do a lookup on a string.
> > >
> > > The existing implementation stores the Components vectors in a struct
> > >
> > >     struct Components {
> > >             uint32_t ct;
> > >             std::vector<uint16_t> r;
> > >             std::vector<uint16_t> gr;
> > >             std::vector<uint16_t> gb;
> > >             std::vector<uint16_t> b;
> > >     };
> > >
> > > This patch instead stores them in a map.
> > >
> > > Now, not an STL guru here, but I presume that if your only concern is
> > > the lookup, a map with 4 elements top shouldn't be that hard.
> > >
> > > If there are implications I'm missing about storing the vectors in the
> > > map instead of storing them in a struct I would be glad to learn about
> > > that.
> >
> > A map with string keys will be significantly less efficient. First the
> > string will be hashed, then the hash will be used in the lookup, and
> 
> Are you sure hashing happen for std::map too or is it just for
> std::unordered_map ?
> 
> As far as I know, std::map sorts the keys by comparing them and
> the default Compare function is std::less<> which for std::string calls
> std::string::operator< which has a complexity linear in the string
> length (1 or 2 chars). Then the sorted key lookup itself should be
> O(log n), which for a map of up to two elements is nothing.
> 
> If using an unordered_map<> then hasing of the key (which [1] doesn't
> report the complexity of) will take place, then the lookup should
> happen in linear time.
> 
> [1] https://en.cppreference.com/cpp/string/basic_string/hash
> > finally the strings will be compared.
> >
> 
> When the LSC tables have to be updated because of a significant change
> in colour temperature we interpolate vectors of thousands of elements
> for the new colour temperature and then memcpy them to the parameters buffer.
> 
> Before reworking anything here I would like to see a comparison of these
> costs to the above described complexity of using a map of 4 elements
> indexed by a string of 1 or 2 characters.
> 
> And no, we're not:
> 
> > > > going great lengths to
> > > > write super optimized code striving for static allocations as much as
> > > > possible
> 
> Both when loading from yaml file and when re-sampling the polynomial
> the destination vectors are dynamically allocatated. This is good, it
> avoids relocations, but it's certainly not "static allocation as much
> as possible" and I don't think anyone complained.

I feel like I need to clarify my initial words. I'm well aware that a
vector is dynamically allocating memory. The thing is that I felt bad
for even having these vectors in the frame context because there is this
"we'd eventually like to do everything static" mantra in my head. With
the "great lengths" I thought more of the "super optimized" aspect. With
static allocation I think more of things like the fixed number of entries
in the FCQueue or the redesigned metadata with preallocated fixed memory
buffers. I'm a big fan of keep it simple and let the language do it's
thing. That is why camshark is full of maps and string lookups.

Fixed FCQueue makes sense as it prevents reallocation during streaming.
The Yaml parsing time 

> 
> > > > I was wondering if it is possible to template that using a static set as
> > > > temüplate param and a fixed size array as container but then you can't
> > > > configure that at init() time and end up with too much template
> > > > magic....no
> > > >
> > > > As a compromise, would it make sense to index the map by an enum, so
> > > > that we could define an enum for the possible types (it will most likely
> > > > ever only be g,r,b,rg,bg (possible ir in the future). That would at
> > > > least replace the string lookup by an int lookup, and we'd only have
> > > > strings at yaml parse time?
> >
> > If we have an index, can't we just make it an array or vector ?
> >
> 
> Each IPA has different indexes that reflect the number of tables the
> hadware supprots ('r' 'g' 'b' for mali, 'r' 'gr' 'gb' 'b' for rkisp1
> etc). The current implementation allows the IPA to specify which keys
> to use to parse the tables (or polynomials) from the tuning file and
> then use the same keys for tables lookup when LSC have to be updated.
> 
> This is of course the most flexible solution. We can provide a less
> flexible interface to IPAs and assume up to 4 vectors are supported
> and let IPA map the array entries to the colour component.
> 
> Again, this seems largely unecessary to me considering the above
> described cost of using a map is justified by a nicer API.

I still think, that replacing the strings with an enum would keep the
same flexibility, add a bit more compile time checking and maybe would
even be a tad more efficient - but as you wrote that gain will be
neglectable.

> 
> > > So is the only concern really comparing strings vs comparing integers
> > > when looking up a key ? In the same code path that memcpys thousands
> > > of values to program the lsc tables ?
> > >
> > > I feel like I'm missing something.
> > >
> > > > > +};
> > > > > +
> > > > > +template<typename T>
> > > > > +class ComponentsMap : public std::map<unsigned int, Components<T>>
> > > > > +{
> > > > > +};
> > > > > +
> > > > > +} /* namespace lsc */
> > > > > +
> > > > > +struct LscDescriptor {
> > > > > +       std::vector<std::string> keys;
> > > > > +       unsigned int numHCells;
> > > > > +       unsigned int numVCells;
> > > > > +       Size sensorSize;
> > > > > +};
> > > > > +
> > > > > +template<typename T, typename U>
> > > > > +class LscImplementation
> > > >
> > > > On v1 Dan commented that U is not used in grid based. But it is used in
> > > > the polynomial case. What about dropping T and defining it as
> > > > U::QuantizedType? That would work for both cases and you get rid of one
> > > > parameter. (And if there is a hardware that has a different register
> > > > size than U::QuantizedType, the current solution would rely on luck,
> > > > too.)
> > >
> > > This one is a good suggestion.
> > >
> > > Right now the template argument T serves only for loading values from
> > > file in a type large enough to accommodate them
> > >
> > > Using U::QuantizedType seems like a good idea.
> > > We're gonna drop it anyway an move to store and load float gains from
> > > the tuning file, but in the interim using Q::QuantizedType seems good
> > > enough.
> > >
> > > > > +{
> > > > > +public:
> > > > > +       virtual ~LscImplementation() = default;
> > > > > +
> > > > > +       virtual int parseLscData(const ValueNode &tuningData,
> > > > > +                                const LscDescriptor &descriptor) = 0;
> > > > > +
> > > > > +       virtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> > > > > +                                                     const std::vector<double> &xPos,
> > > > > +                                                     const std::vector<double> &yPos) = 0;
> > > > > +};
> > > > > +
> > > > > +} /* namespace ipa */
> > > > > +
> > > > > +} /* namespace libcamera */
> >
> > [snip]
> >
> > > > > diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
> > > > > index f607d86c54c3..8d3f1883d6a5 100644
> > > > > --- a/src/ipa/libipa/lsc_polynomial.cpp
> > > > > +++ b/src/ipa/libipa/lsc_polynomial.cpp
> > > > > @@ -1,12 +1,14 @@
> > > > >  /* SPDX-License-Identifier: LGPL-2.1-or-later */
> > > > >  /*
> > > > > - * Copyright (C) 2024, Ideas On Board
> > > > > + * Copyright (C) 2026, Ideas On Board
> > > > >   *
> > > > > - * Polynomial class to represent lens shading correction
> > > > > + * Polynomial based lens shading correction
> > > > >   */
> > > > >
> > > > >  #include "lsc_polynomial.h"
> > > > >
> > > > > +#include <assert.h>
> > > > > +
> > > > >  #include <libcamera/base/log.h>
> > > > >
> > > > >  /**
> > > > > @@ -21,7 +23,7 @@ LOG_DEFINE_CATEGORY(LscPolynomial)
> > > > >  namespace ipa {
> > > > >
> > > > >  /**
> > > > > - * \class LscPolynomial
> > > > > + * \class Polynomial
> > > > >   * \brief Class for handling even polynomials used in lens shading correction
> > > > >   *
> > > > >   * Shading artifacts of camera lenses can be modeled using even radial
> > > > > @@ -31,7 +33,7 @@ namespace ipa {
> > > > >   */
> > > > >
> > > > >  /**
> > > > > - * \fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > > > > + * \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > > > >                       double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> > > > >                       double k4 = 0.0)
> > > > >   * \brief Construct a polynomial using the given coefficients
> > > > > @@ -45,7 +47,13 @@ namespace ipa {
> > > > >   */
> > > > >
> > > > >  /**
> > > > > - * \fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)
> > > > > + * \fn Polynomial::Polynomial(const Polynomial &other)
> > > > > + * \brief Construct a Polynomial by copy
> > > > > + * \param[in] other The Polynomial to copy construct from
> > > > > + */
> > > > > +
> > > > > +/**
> > > > > + * \fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)
> > > > >   * \brief Sample the polynomial at the given normalized pixel position
> > > > >   *
> > > > >   * This functions samples the polynomial at the given pixel position divided by
> > > > > @@ -55,9 +63,21 @@ namespace ipa {
> > > > >   * \param y y position in normalized coordinates
> > > > >   * \return The sampled value
> > > > >   */
> > > > > +double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
> > > > > +{
> > > > > +       double dx = x - cnx_;
> > > > > +       double dy = y - cny_;
> > > > > +       double r = sqrt(dx * dx + dy * dy);
> > > > > +       double res = 1.0;
> > > > > +
> > > > > +       for (unsigned int i = 0; i < coefficients_.size(); i++)
> > > > > +               res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> > > > > +
> > > > > +       return res;
> > > > > +}
> > > >
> > > > These are plain refactorings. I see that the whole libipa rework
> > > > is difficult to do in small patches, but I think it would make review
> > > > (and later history reading) easier if we'd cut out these refactorings
> > > > into separate preparatory patches.
> > >
> > > I think we all knew that this work was going to be hard to review as
> > > it was hard to implement.
> > >
> > > I again suggest to look at the final result more than the diff.
> >
> > :-(
> >
> > > I could break the patches down in small non-compilable units if it
> > > helps.

I'm not sure if non-compilable patches really help. But I think there
are logical units that could be cut out (Like going from members in the
components to a map) in a compilable way. I feel your pain and am at the
same time a bit concerned that reading the history will be a pain if we
do everything in one big step.

Best regards,
Stefan

> >
> > It will help review, if those smaller patches are self-contained and can
> > be reviewed on their own, even if they don't necessarily compile on
> > their own.
> >
> > > > >
> > > > >  /**
> > > > > - * \fn LscPolynomial::getM()
> > > > > + * \fn Polynomial::getM()
> > > > >   * \brief Get the value m as described in the dng specification
> > > > >   *
> > > > >   * Returns m according to dng spec. m represents the Euclidean distance
> >
> > [snip]
> >
> > > > > diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
> > > > > index 0a256e225327..7c656d95acfb 100644
> > > > > --- a/src/ipa/rkisp1/algorithms/lsc.h
> > > > > +++ b/src/ipa/rkisp1/algorithms/lsc.h
> > > > > @@ -2,17 +2,23 @@
> > > > >  /*
> > > > >   * Copyright (C) 2021-2022, Ideas On Board
> > > > >   *
> > > > > - * RkISP1 Lens Shading Correction control
> > > > > + * RkISP1 Lens Shading Correction algorithm
> > > > >   */
> > > > >
> > > > >  #pragma once
> > > > >
> > > > > -#include <map>
> > > > > -#include <memory>
> > > > > +#include <vector>
> > > > >
> > > > > -#include "libipa/interpolator.h"
> > > > > +#include <linux/rkisp1-config.h>
> > > > > +
> > > > > +#include "libcamera/internal/value_node.h"
> > > > > +
> > > > > +#include "libipa/fixedpoint.h"
> > > > > +#include "libipa/lsc.h"
> > > > >
> > > > >  #include "algorithm.h"
> > > > > +#include "ipa_context.h"
> > > > > +#include "params.h"
> > > > >
> > > > >  namespace libcamera {
> > > > >
> > > > > @@ -37,39 +43,28 @@ public:
> > > > >                      const rkisp1_stat_buffer *stats,
> > > > >                      ControlList &metadata) override;
> > > > >
> > > > > -       struct Components {
> > > > > -               std::vector<uint16_t> r;
> > > > > -               std::vector<uint16_t> gr;
> > > > > -               std::vector<uint16_t> gb;
> > > > > -               std::vector<uint16_t> b;
> > > > > -       };
> > > > > -
> > > > > -       class ShadingDescriptor
> > > > > -       {
> > > > > -       public:
> > > > > -               virtual ~ShadingDescriptor() = default;
> > > > > -               virtual Components sampleForCrop(const Rectangle &cropRectangle,
> > > > > -                                                Span<const double> xSizes,
> > > > > -                                                Span<const double> ySizes) = 0;
> > > > > -       };
> > > > > -
> > > > > -       using ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;
> > > > > -
> > > > >  private:
> > > > > +       std::vector<double> parseSizes(const ValueNode &tuningData,
> > > > > +                                      const char *prop);
> > > > > +       std::vector<double> sizesToPositions(Span<const double> sizes);
> > > > > +
> > > > >         void setParameters(rkisp1_cif_isp_lsc_config &config);
> > > > > -       void copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);
> > > > > +       void copyTable(rkisp1_cif_isp_lsc_config &config,
> > > > > +                      const ipa::lsc::Components<uint16_t> &set0);
> > > > >
> > > > > -       ShadingDescriptorMap shadingDescriptors_;
> > > > > -       ipa::Interpolator<Components> sets_;
> > > > >         std::vector<double> xSize_;
> > > > >         std::vector<double> ySize_;
> > > > >         uint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > >         uint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > >         uint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > >         uint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > +       std::vector<double> xPos_;
> > > > > +       std::vector<double> yPos_;
> > > > >
> > > > >         unsigned int lastAppliedCt_;
> > > > >         unsigned int lastAppliedQuantizedCt_;
> > > > > +
> > > > > +       LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;
> > > >
> > > > I don't really like specifying the fixed format at compile time, as we
> > > > will have a hard time to implement support for different formats at
> > > > runtime like Mali supports. But I also don't see an easy way out of
> > > > that.
> > > >
> > > > >  };
> > > > >
> > > > >  } /* namespace ipa::rkisp1::algorithms */
> > > > > diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
> > > > > index cd97e10bcf2b..005f4102b4f6 100644
> > > > > --- a/src/ipa/rkisp1/ipa_context.h
> > > > > +++ b/src/ipa/rkisp1/ipa_context.h
> > > > > @@ -30,6 +30,7 @@
> > > > >  #include "libipa/ccm.h"
> > > > >  #include "libipa/fc_queue.h"
> > > > >  #include "libipa/fixedpoint.h"
> > > > > +#include "libipa/lsc.h"
> > > > >
> > > > >  namespace libcamera {
> > > > >
> > > > > @@ -138,9 +139,7 @@ struct IPAActiveState {
> > > > >                 double strength;
> > > > >         } wdr;
> > > > >
> > > > > -       struct {
> > > > > -               bool enabled;
> > > > > -       } lsc;
> > > > > +       ipa::lsc::ActiveState lsc;
> > > >
> > > > That will really ease IPA development.
> > > >
> > > > >  };
> > > > >
> > > > >  struct IPAFrameContext : public FrameContext {
> > > > > @@ -213,10 +212,7 @@ struct IPAFrameContext : public FrameContext {
> > > > >                 double gain;
> > > > >         } wdr;
> > > > >
> > > > > -       struct {
> > > > > -               bool enabled;
> > > > > -               bool update;
> > > > > -       } lsc;
> > > > > +       ipa::lsc::FrameContext lsc;
> > > > >  };
> > > >
> > > > This is a really massive patch and I wonder if it would be possible to
> > > > split it out into multiple changes (also in case we have regressions). I
> > > > know this is more work, but otoh only rkisp1 uses that. So it would be
> > > > possible?
> > >
> > > Everything is possible.
> > >
> > > I guess we'll miss the next release though.
> >
> > Don't worry about releases. They are just numbers. Let's move forward
> > with this work, give it review priority, and merge it when it will be
> > ready.
> >
> > > > >  struct IPAContext {
> >
> > --
> > Regards,
> >
> > Laurent Pinchart
Stefan Klug June 29, 2026, 9:05 a.m. UTC | #10
Hi Jacopo,

Quoting Stefan Klug (2026-06-29 10:04:31)
> Hi Jacopo,
> 
> Quoting Jacopo Mondi (2026-06-27 11:24:21)
> > Hi Laurent
> > 
> > On Fri, Jun 26, 2026 at 10:51:51PM +0300, Laurent Pinchart wrote:
> > > On Fri, Jun 26, 2026 at 04:26:53PM +0200, Jacopo Mondi wrote:
> > > > On Fri, Jun 26, 2026 at 11:18:12AM +0200, Stefan Klug wrote:
> > > > > Quoting Jacopo Mondi (2026-06-15 16:05:34)
> > > > > > Introduce an Lsc algorithm implementation in libIPA.
> > > > > >
> > > > > > The LscAlgorithm class supports two algorithms: grid-based lsc and
> > > > > > polynomial-based lsc.
> > > > > >
> > > > > > Both implementations have been generalized from the existing ones in the
> > > > > > RkISP1 IPA.
> > > > > >
> > > > > > Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> > > > > > ---
> > > > > >  src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++
> > > > > >  src/ipa/libipa/lsc.h              | 145 +++++++++++++
> > > > > >  src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++
> > > > > >  src/ipa/libipa/lsc_base.h         |  59 ++++++
> > > > > >  src/ipa/libipa/lsc_grid.cpp       |  33 +++
> > > > > >  src/ipa/libipa/lsc_grid.h         | 113 ++++++++++
> > > > > >  src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-
> > > > > >  src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----
> > > > > >  src/ipa/libipa/meson.build        |   5 +
> > > > > >  src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------
> > > > > >  src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--
> > > > > >  src/ipa/rkisp1/ipa_context.h      |  10 +-
> > > > > >  12 files changed, 1126 insertions(+), 450 deletions(-)
> > > > > >
> > > > > > diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
> > > > > > new file mode 100644
> > > > > > index 000000000000..ae713a776778
> > > > > > --- /dev/null
> > > > > > +++ b/src/ipa/libipa/lsc.cpp
> > > > > > @@ -0,0 +1,322 @@
> > >
> > > [snip]
> > >
> > > > > > +/**
> > > > > > + * \class LscAlgorithm
> > > > > > + * \brief libIPA lsc algorithm implementation
> > > > > > + * \tparam T The type used to store the gain values when loaded from tuning file
> > > > > > + * \tparam U The fixedpoint lsc engine register format
> > > > > > + *
> > > > > > + * Due to the optical characteristics of the lens, the light intensity received
> > > > > > + * by the sensor is not uniform. The Lens Shading Correction algorithm applies
> > > > > > + * multipliers to all pixels to compensate for the lens shading effect.
> > > > > > + *
> > > > > > + * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.
> > > > > > + * It provides support for parsing the tuning file content and generate tables
> > > > > > + * of per-colour temperature gains that IPA algorithms can use to program their
> > > > > > + * lsc engine.
> > > > > > + *
> > > > > > + * The init() function parses the tuning file and loads the gain tables either
> > > > > > + * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain
> > > > > > + * tables are organized per-colour temperature with per-colour components gain
> > > > > > + * vectors or polynomial coefficients. The colour components names are
> > > > > > + * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc
> > > > > > + * engine support 4 colour components (r, gr, gb, b), some only support 3 colour
> > > > > > + * components (r, g, b). The name (and number) of the expected colour components
> > > > > > + * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys
> > > > > > + * field.
> > > > > > + *
> > > > > > + * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour
> > > > > > + * components. The gain table has been here omitted, but the expected number
> > > > >
> > > > > Nit: s/here omitted/omitted here/
> > > >
> > > > I think 'here omitted' is correct
> > >
> > > "omitted here" sounds more correct.
> > >
> > > > > > + * of entries has be equal to
> > > > >
> > > > > Nit: has to be
> > > > >
> > > > > > + * LscDescriptor::numHCells * LscDescriptor::numVCells.
> > > > > > + *
> > > > > > + * \code{.yaml}
> > > > > > + * - Lsc:
> > > > > > + *    sets:
> > > > > > + *      - ct: 2500
> > > > > > + *        r: [
> > > > > > + *             .. gains table omitted..
> > > > > > + *        ]
> > > > > > + *        g: [
> > > > > > + *             .. gains table omitted..
> > > > > > + *        ]
> > > > > > + *        b: [
> > > > > > + *             .. gains table omitted..
> > > > > > + *        ]
> > > > > > + *      - ct: 6500
> > > > > > + *        r: [
> > > > > > + *             .. gains table omitted..
> > > > > > + *        ]
> > > > > > + *        g: [
> > > > > > + *             .. gains table omitted..
> > > > > > + *        ]
> > > > > > + *        b: [
> > > > > > + *             .. gains table omitted..
> > > > > > + *        ]
> > > > > > + * \endcode
> > > > > > + *
> > > > > > + * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'
> > > > > > + * colour components:
> > > > > > + *
> > > > > > + * \code{.yaml}
> > > > > > + * - Lsc:
> > > > > > + *    type: "polynomial"
> > > > > > + *    sets:
> > > > > > + *      - ct: 2500
> > > > > > + *        r:
> > > > > > + *          cx: 0.5006571711950275
> > > > > > + *          cy: 0.510093737499277
> > > > > > + *          k0: 1.5393282208428813
> > > > > > + *          k1: -1.1434559757908016
> > > > > > + *          k2: 4.332602305814554
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + *        gr:
> > > > > > + *          cx: 0.5009320529087338
> > > > > > + *          cy: 0.511208038949085
> > > > > > + *          k0: 1.5634738574805407
> > > > > > + *          k1: -1.5623484259968348
> > > > > > + *          k2: 4.846686073656501
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + *        gb:
> > > > > > + *          cx: 0.5012013290343839
> > > > > > + *          cy: 0.5128251541578288
> > > > > > + *          k0: 1.526147944919103
> > > > > > + *          k1: -1.4316976083689723
> > > > > > + *          k2: 4.792604063222728
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + *        b:
> > > > > > + *          cx: 0.49864139511067784
> > > > > > + *          cy: 0.5162095081739346
> > > > > > + *          k0: 1.0405245474038738
> > > > > > + *          k1: 0.05618339879447103
> > > > > > + *          k2: 1.8792813594001752
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + *      - ct: 6000
> > > > > > + *        r:
> > > > > > + *          cx: 0.5006202239353942
> > > > > > + *          cy: 0.5099531318307661
> > > > > > + *          k0: 1.4702946023945032
> > > > > > + *          k1: -0.8893767547927631
> > > > > > + *          k2: 3.920547732201387
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + *        gr:
> > > > > > + *          cx: 0.500907874178317
> > > > > > + *          cy: 0.511084916024106
> > > > > > + *          k0: 1.5336172760559457
> > > > > > + *          k1: -1.39964026514435
> > > > > > + *          k2: 4.565487728954618
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + *        gb:
> > > > > > + *          cx: 0.5011898608900477
> > > > > > + *          cy: 0.5126797906745105
> > > > > > + *          k0: 1.5013145790354843
> > > > > > + *          k1: -1.2747407173754124
> > > > > > + *          k2: 4.514682876897286
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + *        b:
> > > > > > + *          cx: 0.4987561413116136
> > > > > > + *          cy: 0.5159619420778772
> > > > > > + *          k0: 1.0102986422191802
> > > > > > + *          k1: 0.13263449763985727
> > > > > > + *          k2: 1.686556107316064
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + * \endcode
> > > > > > + *
> > > > > > + * The lsc tables or the polynomial definition are generated at tuning time
> > > > > > + * using an image of known resolution which needs to be specified in
> > > > > > + * LscDescriptor::sensorSize.
> > > > > > + *
> > > > > > + * At LscAlgorithm::configure() time the lsc tables are re-sampled on the
> > > > > > + * sensor's crop rectangle in use to adapt them to the configuration in use for
> > > > > > + * a streaming session. Polynomial lsc tables support re-sampling and can be
> > > > > > + * applied to any sensor configuration. Grid-based lsc tables cannot be
> > > > > > + * re-sampled and the configuration as parsed from the tuning file is used for
> > > > > > + * all sensor configurations providing best-effort results.
> > > > > > + *
> > > > > > + * When the IPA algorithms wants to get access to the (resampled) tables to
> > > > > > + * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get
> > > > > > + * an lsc table interpolated by the LscAlgorithm class for the specified colour
> > > > > > + * temperature. If the algorithm wants to access the non-interpolated tables it
> > > > > > + * can retrieve them using LscAlgorithm::getComponents().
> > > > > > + */
> > >
> > > [snip]
> > >
> > > > > > diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
> > > > > > new file mode 100644
> > > > > > index 000000000000..aecae305aa9c
> > > > > > --- /dev/null
> > > > > > +++ b/src/ipa/libipa/lsc_base.h
> > > > > > @@ -0,0 +1,59 @@
> > > > > > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > > > > > +/*
> > > > > > + * Copyright (C) 2026 Ideas on Board Oy
> > > > > > + *
> > > > > > + * Base classes and types for LSC algorithms implementations
> > > > > > + */
> > > > > > +
> > > > > > +#pragma once
> > > > > > +
> > > > > > +#include <map>
> > > > > > +#include <string>
> > > > > > +#include <vector>
> > > > > > +
> > > > > > +#include <libcamera/geometry.h>
> > > > > > +
> > > > > > +#include "libcamera/internal/value_node.h"
> > > > > > +
> > > > > > +namespace libcamera {
> > > > > > +
> > > > > > +namespace ipa {
> > > > > > +
> > > > > > +namespace lsc {
> > > > > > +
> > > > > > +template<typename T>
> > > > > > +class Components : public std::map<std::string, std::vector<T>>
> > > > > > +{
> > > > >
> > > > > I'm a bit uneasy here. On the one hand we are going great lengths to
> > > > > write super optimized code striving for static allocations as much as
> > > > > possible and now we add a string based map lookup that is run per frame.
> > > >
> > > > How are 'string based lookup' and 'static memory allocation' related ?
> > > >
> > > > > On the other hand I don't actually know how expensive the lookup is.
> > > >
> > > > So, Component is used at init to parse the tuning file. Fine.
> > > >
> > > > At configure a set of Components is re-sampled (polynomial) or
> > > > returned as parsed from config file. Fine.
> > > >
> > > > Now, the run-time.
> > > >
> > > > I presume you're thinking at (using rkisp1 as an example)
> > > >
> > > >  void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
> > > > -                                     const Components &set)
> > > > +                                     const ipa::lsc::Components<uint16_t> &set)
> > > >  {
> > > > -       std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
> > > > -       std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
> > > > -       std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
> > > > -       std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
> > > > +       const auto &r = set.at("r");
> > > > +       std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
> > > > +       const auto &gr = set.at("gr");
> > > > +       std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
> > > > +       const auto &gb = set.at("gb");
> > > > +       std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
> > > > +       const auto &b = set.at("b");
> > > > +       std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
> > > >  }
> > > >
> > > > yes, we do a lookup on a string.
> > > >
> > > > The existing implementation stores the Components vectors in a struct
> > > >
> > > >     struct Components {
> > > >             uint32_t ct;
> > > >             std::vector<uint16_t> r;
> > > >             std::vector<uint16_t> gr;
> > > >             std::vector<uint16_t> gb;
> > > >             std::vector<uint16_t> b;
> > > >     };
> > > >
> > > > This patch instead stores them in a map.
> > > >
> > > > Now, not an STL guru here, but I presume that if your only concern is
> > > > the lookup, a map with 4 elements top shouldn't be that hard.
> > > >
> > > > If there are implications I'm missing about storing the vectors in the
> > > > map instead of storing them in a struct I would be glad to learn about
> > > > that.
> > >
> > > A map with string keys will be significantly less efficient. First the
> > > string will be hashed, then the hash will be used in the lookup, and
> > 
> > Are you sure hashing happen for std::map too or is it just for
> > std::unordered_map ?
> > 
> > As far as I know, std::map sorts the keys by comparing them and
> > the default Compare function is std::less<> which for std::string calls
> > std::string::operator< which has a complexity linear in the string
> > length (1 or 2 chars). Then the sorted key lookup itself should be
> > O(log n), which for a map of up to two elements is nothing.
> > 
> > If using an unordered_map<> then hasing of the key (which [1] doesn't
> > report the complexity of) will take place, then the lookup should
> > happen in linear time.
> > 
> > [1] https://en.cppreference.com/cpp/string/basic_string/hash
> > > finally the strings will be compared.
> > >
> > 
> > When the LSC tables have to be updated because of a significant change
> > in colour temperature we interpolate vectors of thousands of elements
> > for the new colour temperature and then memcpy them to the parameters buffer.
> > 
> > Before reworking anything here I would like to see a comparison of these
> > costs to the above described complexity of using a map of 4 elements
> > indexed by a string of 1 or 2 characters.
> > 
> > And no, we're not:
> > 
> > > > > going great lengths to
> > > > > write super optimized code striving for static allocations as much as
> > > > > possible
> > 
> > Both when loading from yaml file and when re-sampling the polynomial
> > the destination vectors are dynamically allocatated. This is good, it
> > avoids relocations, but it's certainly not "static allocation as much
> > as possible" and I don't think anyone complained.
> 
> I feel like I need to clarify my initial words. I'm well aware that a
> vector is dynamically allocating memory. The thing is that I felt bad
> for even having these vectors in the frame context because there is this
> "we'd eventually like to do everything static" mantra in my head. With
> the "great lengths" I thought more of the "super optimized" aspect. With
> static allocation I think more of things like the fixed number of entries
> in the FCQueue or the redesigned metadata with preallocated fixed memory
> buffers. I'm a big fan of keep it simple and let the language do it's
> thing. That is why camshark is full of maps and string lookups.
> 
> Fixed FCQueue makes sense as it prevents reallocation during streaming.
> The Yaml parsing time 

I got distracted and sent the email without finishing this sentence. The
Yaml parsing is before streaming, so I don't see a problem there. The
space for the interpolated lsc data is afaikt also only allocated once
in the interpolator and I don't see a way how to get rid of the copy
into the params buffer. So I agree, everything is fine, we don't reallocate
but it is not static either.

> 
> > 
> > > > > I was wondering if it is possible to template that using a static set as
> > > > > temüplate param and a fixed size array as container but then you can't
> > > > > configure that at init() time and end up with too much template
> > > > > magic....no
> > > > >
> > > > > As a compromise, would it make sense to index the map by an enum, so
> > > > > that we could define an enum for the possible types (it will most likely
> > > > > ever only be g,r,b,rg,bg (possible ir in the future). That would at
> > > > > least replace the string lookup by an int lookup, and we'd only have
> > > > > strings at yaml parse time?
> > >
> > > If we have an index, can't we just make it an array or vector ?
> > >
> > 
> > Each IPA has different indexes that reflect the number of tables the
> > hadware supprots ('r' 'g' 'b' for mali, 'r' 'gr' 'gb' 'b' for rkisp1
> > etc). The current implementation allows the IPA to specify which keys
> > to use to parse the tables (or polynomials) from the tuning file and
> > then use the same keys for tables lookup when LSC have to be updated.
> > 
> > This is of course the most flexible solution. We can provide a less
> > flexible interface to IPAs and assume up to 4 vectors are supported
> > and let IPA map the array entries to the colour component.
> > 
> > Again, this seems largely unecessary to me considering the above
> > described cost of using a map is justified by a nicer API.
> 
> I still think, that replacing the strings with an enum would keep the
> same flexibility, add a bit more compile time checking and maybe would
> even be a tad more efficient - but as you wrote that gain will be
> neglectable.
> 
> > 
> > > > So is the only concern really comparing strings vs comparing integers
> > > > when looking up a key ? In the same code path that memcpys thousands
> > > > of values to program the lsc tables ?
> > > >
> > > > I feel like I'm missing something.
> > > >
> > > > > > +};
> > > > > > +
> > > > > > +template<typename T>
> > > > > > +class ComponentsMap : public std::map<unsigned int, Components<T>>
> > > > > > +{
> > > > > > +};
> > > > > > +
> > > > > > +} /* namespace lsc */
> > > > > > +
> > > > > > +struct LscDescriptor {
> > > > > > +       std::vector<std::string> keys;
> > > > > > +       unsigned int numHCells;
> > > > > > +       unsigned int numVCells;
> > > > > > +       Size sensorSize;
> > > > > > +};
> > > > > > +
> > > > > > +template<typename T, typename U>
> > > > > > +class LscImplementation
> > > > >
> > > > > On v1 Dan commented that U is not used in grid based. But it is used in
> > > > > the polynomial case. What about dropping T and defining it as
> > > > > U::QuantizedType? That would work for both cases and you get rid of one
> > > > > parameter. (And if there is a hardware that has a different register
> > > > > size than U::QuantizedType, the current solution would rely on luck,
> > > > > too.)
> > > >
> > > > This one is a good suggestion.
> > > >
> > > > Right now the template argument T serves only for loading values from
> > > > file in a type large enough to accommodate them
> > > >
> > > > Using U::QuantizedType seems like a good idea.
> > > > We're gonna drop it anyway an move to store and load float gains from
> > > > the tuning file, but in the interim using Q::QuantizedType seems good
> > > > enough.
> > > >
> > > > > > +{
> > > > > > +public:
> > > > > > +       virtual ~LscImplementation() = default;
> > > > > > +
> > > > > > +       virtual int parseLscData(const ValueNode &tuningData,
> > > > > > +                                const LscDescriptor &descriptor) = 0;
> > > > > > +
> > > > > > +       virtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> > > > > > +                                                     const std::vector<double> &xPos,
> > > > > > +                                                     const std::vector<double> &yPos) = 0;
> > > > > > +};
> > > > > > +
> > > > > > +} /* namespace ipa */
> > > > > > +
> > > > > > +} /* namespace libcamera */
> > >
> > > [snip]
> > >
> > > > > > diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
> > > > > > index f607d86c54c3..8d3f1883d6a5 100644
> > > > > > --- a/src/ipa/libipa/lsc_polynomial.cpp
> > > > > > +++ b/src/ipa/libipa/lsc_polynomial.cpp
> > > > > > @@ -1,12 +1,14 @@
> > > > > >  /* SPDX-License-Identifier: LGPL-2.1-or-later */
> > > > > >  /*
> > > > > > - * Copyright (C) 2024, Ideas On Board
> > > > > > + * Copyright (C) 2026, Ideas On Board
> > > > > >   *
> > > > > > - * Polynomial class to represent lens shading correction
> > > > > > + * Polynomial based lens shading correction
> > > > > >   */
> > > > > >
> > > > > >  #include "lsc_polynomial.h"
> > > > > >
> > > > > > +#include <assert.h>
> > > > > > +
> > > > > >  #include <libcamera/base/log.h>
> > > > > >
> > > > > >  /**
> > > > > > @@ -21,7 +23,7 @@ LOG_DEFINE_CATEGORY(LscPolynomial)
> > > > > >  namespace ipa {
> > > > > >
> > > > > >  /**
> > > > > > - * \class LscPolynomial
> > > > > > + * \class Polynomial
> > > > > >   * \brief Class for handling even polynomials used in lens shading correction
> > > > > >   *
> > > > > >   * Shading artifacts of camera lenses can be modeled using even radial
> > > > > > @@ -31,7 +33,7 @@ namespace ipa {
> > > > > >   */
> > > > > >
> > > > > >  /**
> > > > > > - * \fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > > > > > + * \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > > > > >                       double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> > > > > >                       double k4 = 0.0)
> > > > > >   * \brief Construct a polynomial using the given coefficients
> > > > > > @@ -45,7 +47,13 @@ namespace ipa {
> > > > > >   */
> > > > > >
> > > > > >  /**
> > > > > > - * \fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)
> > > > > > + * \fn Polynomial::Polynomial(const Polynomial &other)
> > > > > > + * \brief Construct a Polynomial by copy
> > > > > > + * \param[in] other The Polynomial to copy construct from
> > > > > > + */
> > > > > > +
> > > > > > +/**
> > > > > > + * \fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)
> > > > > >   * \brief Sample the polynomial at the given normalized pixel position
> > > > > >   *
> > > > > >   * This functions samples the polynomial at the given pixel position divided by
> > > > > > @@ -55,9 +63,21 @@ namespace ipa {
> > > > > >   * \param y y position in normalized coordinates
> > > > > >   * \return The sampled value
> > > > > >   */
> > > > > > +double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
> > > > > > +{
> > > > > > +       double dx = x - cnx_;
> > > > > > +       double dy = y - cny_;
> > > > > > +       double r = sqrt(dx * dx + dy * dy);
> > > > > > +       double res = 1.0;
> > > > > > +
> > > > > > +       for (unsigned int i = 0; i < coefficients_.size(); i++)
> > > > > > +               res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> > > > > > +
> > > > > > +       return res;
> > > > > > +}
> > > > >
> > > > > These are plain refactorings. I see that the whole libipa rework
> > > > > is difficult to do in small patches, but I think it would make review
> > > > > (and later history reading) easier if we'd cut out these refactorings
> > > > > into separate preparatory patches.
> > > >
> > > > I think we all knew that this work was going to be hard to review as
> > > > it was hard to implement.
> > > >
> > > > I again suggest to look at the final result more than the diff.
> > >
> > > :-(
> > >
> > > > I could break the patches down in small non-compilable units if it
> > > > helps.
> 
> I'm not sure if non-compilable patches really help. But I think there
> are logical units that could be cut out (Like going from members in the
> components to a map) in a compilable way. I feel your pain and am at the
> same time a bit concerned that reading the history will be a pain if we
> do everything in one big step.
> 
> Best regards,
> Stefan
> 
> > >
> > > It will help review, if those smaller patches are self-contained and can
> > > be reviewed on their own, even if they don't necessarily compile on
> > > their own.
> > >
> > > > > >
> > > > > >  /**
> > > > > > - * \fn LscPolynomial::getM()
> > > > > > + * \fn Polynomial::getM()
> > > > > >   * \brief Get the value m as described in the dng specification
> > > > > >   *
> > > > > >   * Returns m according to dng spec. m represents the Euclidean distance
> > >
> > > [snip]
> > >
> > > > > > diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
> > > > > > index 0a256e225327..7c656d95acfb 100644
> > > > > > --- a/src/ipa/rkisp1/algorithms/lsc.h
> > > > > > +++ b/src/ipa/rkisp1/algorithms/lsc.h
> > > > > > @@ -2,17 +2,23 @@
> > > > > >  /*
> > > > > >   * Copyright (C) 2021-2022, Ideas On Board
> > > > > >   *
> > > > > > - * RkISP1 Lens Shading Correction control
> > > > > > + * RkISP1 Lens Shading Correction algorithm
> > > > > >   */
> > > > > >
> > > > > >  #pragma once
> > > > > >
> > > > > > -#include <map>
> > > > > > -#include <memory>
> > > > > > +#include <vector>
> > > > > >
> > > > > > -#include "libipa/interpolator.h"
> > > > > > +#include <linux/rkisp1-config.h>
> > > > > > +
> > > > > > +#include "libcamera/internal/value_node.h"
> > > > > > +
> > > > > > +#include "libipa/fixedpoint.h"
> > > > > > +#include "libipa/lsc.h"
> > > > > >
> > > > > >  #include "algorithm.h"
> > > > > > +#include "ipa_context.h"
> > > > > > +#include "params.h"
> > > > > >
> > > > > >  namespace libcamera {
> > > > > >
> > > > > > @@ -37,39 +43,28 @@ public:
> > > > > >                      const rkisp1_stat_buffer *stats,
> > > > > >                      ControlList &metadata) override;
> > > > > >
> > > > > > -       struct Components {
> > > > > > -               std::vector<uint16_t> r;
> > > > > > -               std::vector<uint16_t> gr;
> > > > > > -               std::vector<uint16_t> gb;
> > > > > > -               std::vector<uint16_t> b;
> > > > > > -       };
> > > > > > -
> > > > > > -       class ShadingDescriptor
> > > > > > -       {
> > > > > > -       public:
> > > > > > -               virtual ~ShadingDescriptor() = default;
> > > > > > -               virtual Components sampleForCrop(const Rectangle &cropRectangle,
> > > > > > -                                                Span<const double> xSizes,
> > > > > > -                                                Span<const double> ySizes) = 0;
> > > > > > -       };
> > > > > > -
> > > > > > -       using ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;
> > > > > > -
> > > > > >  private:
> > > > > > +       std::vector<double> parseSizes(const ValueNode &tuningData,
> > > > > > +                                      const char *prop);
> > > > > > +       std::vector<double> sizesToPositions(Span<const double> sizes);
> > > > > > +
> > > > > >         void setParameters(rkisp1_cif_isp_lsc_config &config);
> > > > > > -       void copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);
> > > > > > +       void copyTable(rkisp1_cif_isp_lsc_config &config,
> > > > > > +                      const ipa::lsc::Components<uint16_t> &set0);
> > > > > >
> > > > > > -       ShadingDescriptorMap shadingDescriptors_;
> > > > > > -       ipa::Interpolator<Components> sets_;
> > > > > >         std::vector<double> xSize_;
> > > > > >         std::vector<double> ySize_;
> > > > > >         uint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > >         uint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > >         uint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > >         uint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > > +       std::vector<double> xPos_;
> > > > > > +       std::vector<double> yPos_;
> > > > > >
> > > > > >         unsigned int lastAppliedCt_;
> > > > > >         unsigned int lastAppliedQuantizedCt_;
> > > > > > +
> > > > > > +       LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;
> > > > >
> > > > > I don't really like specifying the fixed format at compile time, as we
> > > > > will have a hard time to implement support for different formats at
> > > > > runtime like Mali supports. But I also don't see an easy way out of
> > > > > that.
> > > > >
> > > > > >  };
> > > > > >
> > > > > >  } /* namespace ipa::rkisp1::algorithms */
> > > > > > diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
> > > > > > index cd97e10bcf2b..005f4102b4f6 100644
> > > > > > --- a/src/ipa/rkisp1/ipa_context.h
> > > > > > +++ b/src/ipa/rkisp1/ipa_context.h
> > > > > > @@ -30,6 +30,7 @@
> > > > > >  #include "libipa/ccm.h"
> > > > > >  #include "libipa/fc_queue.h"
> > > > > >  #include "libipa/fixedpoint.h"
> > > > > > +#include "libipa/lsc.h"
> > > > > >
> > > > > >  namespace libcamera {
> > > > > >
> > > > > > @@ -138,9 +139,7 @@ struct IPAActiveState {
> > > > > >                 double strength;
> > > > > >         } wdr;
> > > > > >
> > > > > > -       struct {
> > > > > > -               bool enabled;
> > > > > > -       } lsc;
> > > > > > +       ipa::lsc::ActiveState lsc;
> > > > >
> > > > > That will really ease IPA development.
> > > > >
> > > > > >  };
> > > > > >
> > > > > >  struct IPAFrameContext : public FrameContext {
> > > > > > @@ -213,10 +212,7 @@ struct IPAFrameContext : public FrameContext {
> > > > > >                 double gain;
> > > > > >         } wdr;
> > > > > >
> > > > > > -       struct {
> > > > > > -               bool enabled;
> > > > > > -               bool update;
> > > > > > -       } lsc;
> > > > > > +       ipa::lsc::FrameContext lsc;
> > > > > >  };
> > > > >
> > > > > This is a really massive patch and I wonder if it would be possible to
> > > > > split it out into multiple changes (also in case we have regressions). I
> > > > > know this is more work, but otoh only rkisp1 uses that. So it would be
> > > > > possible?
> > > >
> > > > Everything is possible.
> > > >
> > > > I guess we'll miss the next release though.
> > >
> > > Don't worry about releases. They are just numbers. Let's move forward
> > > with this work, give it review priority, and merge it when it will be
> > > ready.
> > >
> > > > > >  struct IPAContext {
> > >
> > > --
> > > Regards,
> > >
> > > Laurent Pinchart
Jacopo Mondi June 29, 2026, 9:09 a.m. UTC | #11
On Mon, Jun 29, 2026 at 10:04:31AM +0200, Stefan Klug wrote:
> Hi Jacopo,
>
> Quoting Jacopo Mondi (2026-06-27 11:24:21)
> > Hi Laurent
> >
> > On Fri, Jun 26, 2026 at 10:51:51PM +0300, Laurent Pinchart wrote:
> > > On Fri, Jun 26, 2026 at 04:26:53PM +0200, Jacopo Mondi wrote:
> > > > On Fri, Jun 26, 2026 at 11:18:12AM +0200, Stefan Klug wrote:
> > > > > Quoting Jacopo Mondi (2026-06-15 16:05:34)
> > > > > > Introduce an Lsc algorithm implementation in libIPA.
> > > > > >
> > > > > > The LscAlgorithm class supports two algorithms: grid-based lsc and
> > > > > > polynomial-based lsc.
> > > > > >
> > > > > > Both implementations have been generalized from the existing ones in the
> > > > > > RkISP1 IPA.
> > > > > >
> > > > > > Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> > > > > > ---
> > > > > >  src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++
> > > > > >  src/ipa/libipa/lsc.h              | 145 +++++++++++++
> > > > > >  src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++
> > > > > >  src/ipa/libipa/lsc_base.h         |  59 ++++++
> > > > > >  src/ipa/libipa/lsc_grid.cpp       |  33 +++
> > > > > >  src/ipa/libipa/lsc_grid.h         | 113 ++++++++++
> > > > > >  src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-
> > > > > >  src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----
> > > > > >  src/ipa/libipa/meson.build        |   5 +
> > > > > >  src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------
> > > > > >  src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--
> > > > > >  src/ipa/rkisp1/ipa_context.h      |  10 +-
> > > > > >  12 files changed, 1126 insertions(+), 450 deletions(-)
> > > > > >
> > > > > > diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
> > > > > > new file mode 100644
> > > > > > index 000000000000..ae713a776778
> > > > > > --- /dev/null
> > > > > > +++ b/src/ipa/libipa/lsc.cpp
> > > > > > @@ -0,0 +1,322 @@
> > >
> > > [snip]
> > >
> > > > > > +/**
> > > > > > + * \class LscAlgorithm
> > > > > > + * \brief libIPA lsc algorithm implementation
> > > > > > + * \tparam T The type used to store the gain values when loaded from tuning file
> > > > > > + * \tparam U The fixedpoint lsc engine register format
> > > > > > + *
> > > > > > + * Due to the optical characteristics of the lens, the light intensity received
> > > > > > + * by the sensor is not uniform. The Lens Shading Correction algorithm applies
> > > > > > + * multipliers to all pixels to compensate for the lens shading effect.
> > > > > > + *
> > > > > > + * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.
> > > > > > + * It provides support for parsing the tuning file content and generate tables
> > > > > > + * of per-colour temperature gains that IPA algorithms can use to program their
> > > > > > + * lsc engine.
> > > > > > + *
> > > > > > + * The init() function parses the tuning file and loads the gain tables either
> > > > > > + * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain
> > > > > > + * tables are organized per-colour temperature with per-colour components gain
> > > > > > + * vectors or polynomial coefficients. The colour components names are
> > > > > > + * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc
> > > > > > + * engine support 4 colour components (r, gr, gb, b), some only support 3 colour
> > > > > > + * components (r, g, b). The name (and number) of the expected colour components
> > > > > > + * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys
> > > > > > + * field.
> > > > > > + *
> > > > > > + * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour
> > > > > > + * components. The gain table has been here omitted, but the expected number
> > > > >
> > > > > Nit: s/here omitted/omitted here/
> > > >
> > > > I think 'here omitted' is correct
> > >
> > > "omitted here" sounds more correct.
> > >
> > > > > > + * of entries has be equal to
> > > > >
> > > > > Nit: has to be
> > > > >
> > > > > > + * LscDescriptor::numHCells * LscDescriptor::numVCells.
> > > > > > + *
> > > > > > + * \code{.yaml}
> > > > > > + * - Lsc:
> > > > > > + *    sets:
> > > > > > + *      - ct: 2500
> > > > > > + *        r: [
> > > > > > + *             .. gains table omitted..
> > > > > > + *        ]
> > > > > > + *        g: [
> > > > > > + *             .. gains table omitted..
> > > > > > + *        ]
> > > > > > + *        b: [
> > > > > > + *             .. gains table omitted..
> > > > > > + *        ]
> > > > > > + *      - ct: 6500
> > > > > > + *        r: [
> > > > > > + *             .. gains table omitted..
> > > > > > + *        ]
> > > > > > + *        g: [
> > > > > > + *             .. gains table omitted..
> > > > > > + *        ]
> > > > > > + *        b: [
> > > > > > + *             .. gains table omitted..
> > > > > > + *        ]
> > > > > > + * \endcode
> > > > > > + *
> > > > > > + * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'
> > > > > > + * colour components:
> > > > > > + *
> > > > > > + * \code{.yaml}
> > > > > > + * - Lsc:
> > > > > > + *    type: "polynomial"
> > > > > > + *    sets:
> > > > > > + *      - ct: 2500
> > > > > > + *        r:
> > > > > > + *          cx: 0.5006571711950275
> > > > > > + *          cy: 0.510093737499277
> > > > > > + *          k0: 1.5393282208428813
> > > > > > + *          k1: -1.1434559757908016
> > > > > > + *          k2: 4.332602305814554
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + *        gr:
> > > > > > + *          cx: 0.5009320529087338
> > > > > > + *          cy: 0.511208038949085
> > > > > > + *          k0: 1.5634738574805407
> > > > > > + *          k1: -1.5623484259968348
> > > > > > + *          k2: 4.846686073656501
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + *        gb:
> > > > > > + *          cx: 0.5012013290343839
> > > > > > + *          cy: 0.5128251541578288
> > > > > > + *          k0: 1.526147944919103
> > > > > > + *          k1: -1.4316976083689723
> > > > > > + *          k2: 4.792604063222728
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + *        b:
> > > > > > + *          cx: 0.49864139511067784
> > > > > > + *          cy: 0.5162095081739346
> > > > > > + *          k0: 1.0405245474038738
> > > > > > + *          k1: 0.05618339879447103
> > > > > > + *          k2: 1.8792813594001752
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + *      - ct: 6000
> > > > > > + *        r:
> > > > > > + *          cx: 0.5006202239353942
> > > > > > + *          cy: 0.5099531318307661
> > > > > > + *          k0: 1.4702946023945032
> > > > > > + *          k1: -0.8893767547927631
> > > > > > + *          k2: 3.920547732201387
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + *        gr:
> > > > > > + *          cx: 0.500907874178317
> > > > > > + *          cy: 0.511084916024106
> > > > > > + *          k0: 1.5336172760559457
> > > > > > + *          k1: -1.39964026514435
> > > > > > + *          k2: 4.565487728954618
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + *        gb:
> > > > > > + *          cx: 0.5011898608900477
> > > > > > + *          cy: 0.5126797906745105
> > > > > > + *          k0: 1.5013145790354843
> > > > > > + *          k1: -1.2747407173754124
> > > > > > + *          k2: 4.514682876897286
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + *        b:
> > > > > > + *          cx: 0.4987561413116136
> > > > > > + *          cy: 0.5159619420778772
> > > > > > + *          k0: 1.0102986422191802
> > > > > > + *          k1: 0.13263449763985727
> > > > > > + *          k2: 1.686556107316064
> > > > > > + *          k3: 0.0
> > > > > > + *          k4: 0.0
> > > > > > + * \endcode
> > > > > > + *
> > > > > > + * The lsc tables or the polynomial definition are generated at tuning time
> > > > > > + * using an image of known resolution which needs to be specified in
> > > > > > + * LscDescriptor::sensorSize.
> > > > > > + *
> > > > > > + * At LscAlgorithm::configure() time the lsc tables are re-sampled on the
> > > > > > + * sensor's crop rectangle in use to adapt them to the configuration in use for
> > > > > > + * a streaming session. Polynomial lsc tables support re-sampling and can be
> > > > > > + * applied to any sensor configuration. Grid-based lsc tables cannot be
> > > > > > + * re-sampled and the configuration as parsed from the tuning file is used for
> > > > > > + * all sensor configurations providing best-effort results.
> > > > > > + *
> > > > > > + * When the IPA algorithms wants to get access to the (resampled) tables to
> > > > > > + * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get
> > > > > > + * an lsc table interpolated by the LscAlgorithm class for the specified colour
> > > > > > + * temperature. If the algorithm wants to access the non-interpolated tables it
> > > > > > + * can retrieve them using LscAlgorithm::getComponents().
> > > > > > + */
> > >
> > > [snip]
> > >
> > > > > > diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
> > > > > > new file mode 100644
> > > > > > index 000000000000..aecae305aa9c
> > > > > > --- /dev/null
> > > > > > +++ b/src/ipa/libipa/lsc_base.h
> > > > > > @@ -0,0 +1,59 @@
> > > > > > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > > > > > +/*
> > > > > > + * Copyright (C) 2026 Ideas on Board Oy
> > > > > > + *
> > > > > > + * Base classes and types for LSC algorithms implementations
> > > > > > + */
> > > > > > +
> > > > > > +#pragma once
> > > > > > +
> > > > > > +#include <map>
> > > > > > +#include <string>
> > > > > > +#include <vector>
> > > > > > +
> > > > > > +#include <libcamera/geometry.h>
> > > > > > +
> > > > > > +#include "libcamera/internal/value_node.h"
> > > > > > +
> > > > > > +namespace libcamera {
> > > > > > +
> > > > > > +namespace ipa {
> > > > > > +
> > > > > > +namespace lsc {
> > > > > > +
> > > > > > +template<typename T>
> > > > > > +class Components : public std::map<std::string, std::vector<T>>
> > > > > > +{
> > > > >
> > > > > I'm a bit uneasy here. On the one hand we are going great lengths to
> > > > > write super optimized code striving for static allocations as much as
> > > > > possible and now we add a string based map lookup that is run per frame.
> > > >
> > > > How are 'string based lookup' and 'static memory allocation' related ?
> > > >
> > > > > On the other hand I don't actually know how expensive the lookup is.
> > > >
> > > > So, Component is used at init to parse the tuning file. Fine.
> > > >
> > > > At configure a set of Components is re-sampled (polynomial) or
> > > > returned as parsed from config file. Fine.
> > > >
> > > > Now, the run-time.
> > > >
> > > > I presume you're thinking at (using rkisp1 as an example)
> > > >
> > > >  void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
> > > > -                                     const Components &set)
> > > > +                                     const ipa::lsc::Components<uint16_t> &set)
> > > >  {
> > > > -       std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
> > > > -       std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
> > > > -       std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
> > > > -       std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
> > > > +       const auto &r = set.at("r");
> > > > +       std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
> > > > +       const auto &gr = set.at("gr");
> > > > +       std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
> > > > +       const auto &gb = set.at("gb");
> > > > +       std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
> > > > +       const auto &b = set.at("b");
> > > > +       std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
> > > >  }
> > > >
> > > > yes, we do a lookup on a string.
> > > >
> > > > The existing implementation stores the Components vectors in a struct
> > > >
> > > >     struct Components {
> > > >             uint32_t ct;
> > > >             std::vector<uint16_t> r;
> > > >             std::vector<uint16_t> gr;
> > > >             std::vector<uint16_t> gb;
> > > >             std::vector<uint16_t> b;
> > > >     };
> > > >
> > > > This patch instead stores them in a map.
> > > >
> > > > Now, not an STL guru here, but I presume that if your only concern is
> > > > the lookup, a map with 4 elements top shouldn't be that hard.
> > > >
> > > > If there are implications I'm missing about storing the vectors in the
> > > > map instead of storing them in a struct I would be glad to learn about
> > > > that.
> > >
> > > A map with string keys will be significantly less efficient. First the
> > > string will be hashed, then the hash will be used in the lookup, and
> >
> > Are you sure hashing happen for std::map too or is it just for
> > std::unordered_map ?
> >
> > As far as I know, std::map sorts the keys by comparing them and
> > the default Compare function is std::less<> which for std::string calls
> > std::string::operator< which has a complexity linear in the string
> > length (1 or 2 chars). Then the sorted key lookup itself should be
> > O(log n), which for a map of up to two elements is nothing.
> >
> > If using an unordered_map<> then hasing of the key (which [1] doesn't
> > report the complexity of) will take place, then the lookup should
> > happen in linear time.
> >
> > [1] https://en.cppreference.com/cpp/string/basic_string/hash
> > > finally the strings will be compared.
> > >
> >
> > When the LSC tables have to be updated because of a significant change
> > in colour temperature we interpolate vectors of thousands of elements
> > for the new colour temperature and then memcpy them to the parameters buffer.
> >
> > Before reworking anything here I would like to see a comparison of these
> > costs to the above described complexity of using a map of 4 elements
> > indexed by a string of 1 or 2 characters.
> >
> > And no, we're not:
> >
> > > > > going great lengths to
> > > > > write super optimized code striving for static allocations as much as
> > > > > possible
> >
> > Both when loading from yaml file and when re-sampling the polynomial
> > the destination vectors are dynamically allocatated. This is good, it
> > avoids relocations, but it's certainly not "static allocation as much
> > as possible" and I don't think anyone complained.
>
> I feel like I need to clarify my initial words. I'm well aware that a
> vector is dynamically allocating memory. The thing is that I felt bad
> for even having these vectors in the frame context because there is this
> "we'd eventually like to do everything static" mantra in my head. With
> the "great lengths" I thought more of the "super optimized" aspect. With
> static allocation I think more of things like the fixed number of entries
> in the FCQueue or the redesigned metadata with preallocated fixed memory
> buffers. I'm a big fan of keep it simple and let the language do it's
> thing. That is why camshark is full of maps and string lookups.
>
> Fixed FCQueue makes sense as it prevents reallocation during streaming.
> The Yaml parsing time
>

Ok.

How is any of this related to this patch and the string lookup though ?

> >
> > > > > I was wondering if it is possible to template that using a static set as
> > > > > temüplate param and a fixed size array as container but then you can't
> > > > > configure that at init() time and end up with too much template
> > > > > magic....no
> > > > >
> > > > > As a compromise, would it make sense to index the map by an enum, so
> > > > > that we could define an enum for the possible types (it will most likely
> > > > > ever only be g,r,b,rg,bg (possible ir in the future). That would at
> > > > > least replace the string lookup by an int lookup, and we'd only have
> > > > > strings at yaml parse time?
> > >
> > > If we have an index, can't we just make it an array or vector ?
> > >
> >
> > Each IPA has different indexes that reflect the number of tables the
> > hadware supprots ('r' 'g' 'b' for mali, 'r' 'gr' 'gb' 'b' for rkisp1
> > etc). The current implementation allows the IPA to specify which keys
> > to use to parse the tables (or polynomials) from the tuning file and
> > then use the same keys for tables lookup when LSC have to be updated.
> >
> > This is of course the most flexible solution. We can provide a less
> > flexible interface to IPAs and assume up to 4 vectors are supported
> > and let IPA map the array entries to the colour component.
> >
> > Again, this seems largely unecessary to me considering the above
> > described cost of using a map is justified by a nicer API.
>
> I still think, that replacing the strings with an enum would keep the
> same flexibility, add a bit more compile time checking and maybe would
> even be a tad more efficient - but as you wrote that gain will be
> neglectable.
>

Yes, neglectable, even more so considering what happens after the
lookup (interpolation and mem copies).

I need to pass the string keys to parse the tuning file in at init()
time anyway and the IPA using the same identifiers for the lookup is
way nicer as an API instead of having to mix strings and enumerated
types.

For the compile time checking, I don't think this is a concern. We're
not talking of application provided input or application facing APIs,
we're talking about in-tree IPA code, it's not like you can write and
run an IPA and get a key identifier wrong. You'll notice quite soon I
guess (and in case of a wrong tuning file, you'll fail at init() time
anyway, so you'll never get to do a lookup).

Honestly, I would like to see more compelling arguments for changing
this implementation before doing it.

> >
> > > > So is the only concern really comparing strings vs comparing integers
> > > > when looking up a key ? In the same code path that memcpys thousands
> > > > of values to program the lsc tables ?
> > > >
> > > > I feel like I'm missing something.
> > > >
> > > > > > +};
> > > > > > +
> > > > > > +template<typename T>
> > > > > > +class ComponentsMap : public std::map<unsigned int, Components<T>>
> > > > > > +{
> > > > > > +};
> > > > > > +
> > > > > > +} /* namespace lsc */
> > > > > > +
> > > > > > +struct LscDescriptor {
> > > > > > +       std::vector<std::string> keys;
> > > > > > +       unsigned int numHCells;
> > > > > > +       unsigned int numVCells;
> > > > > > +       Size sensorSize;
> > > > > > +};
> > > > > > +
> > > > > > +template<typename T, typename U>
> > > > > > +class LscImplementation
> > > > >
> > > > > On v1 Dan commented that U is not used in grid based. But it is used in
> > > > > the polynomial case. What about dropping T and defining it as
> > > > > U::QuantizedType? That would work for both cases and you get rid of one
> > > > > parameter. (And if there is a hardware that has a different register
> > > > > size than U::QuantizedType, the current solution would rely on luck,
> > > > > too.)
> > > >
> > > > This one is a good suggestion.
> > > >
> > > > Right now the template argument T serves only for loading values from
> > > > file in a type large enough to accommodate them
> > > >
> > > > Using U::QuantizedType seems like a good idea.
> > > > We're gonna drop it anyway an move to store and load float gains from
> > > > the tuning file, but in the interim using Q::QuantizedType seems good
> > > > enough.
> > > >
> > > > > > +{
> > > > > > +public:
> > > > > > +       virtual ~LscImplementation() = default;
> > > > > > +
> > > > > > +       virtual int parseLscData(const ValueNode &tuningData,
> > > > > > +                                const LscDescriptor &descriptor) = 0;
> > > > > > +
> > > > > > +       virtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> > > > > > +                                                     const std::vector<double> &xPos,
> > > > > > +                                                     const std::vector<double> &yPos) = 0;
> > > > > > +};
> > > > > > +
> > > > > > +} /* namespace ipa */
> > > > > > +
> > > > > > +} /* namespace libcamera */
> > >
> > > [snip]
> > >
> > > > > > diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
> > > > > > index f607d86c54c3..8d3f1883d6a5 100644
> > > > > > --- a/src/ipa/libipa/lsc_polynomial.cpp
> > > > > > +++ b/src/ipa/libipa/lsc_polynomial.cpp
> > > > > > @@ -1,12 +1,14 @@
> > > > > >  /* SPDX-License-Identifier: LGPL-2.1-or-later */
> > > > > >  /*
> > > > > > - * Copyright (C) 2024, Ideas On Board
> > > > > > + * Copyright (C) 2026, Ideas On Board
> > > > > >   *
> > > > > > - * Polynomial class to represent lens shading correction
> > > > > > + * Polynomial based lens shading correction
> > > > > >   */
> > > > > >
> > > > > >  #include "lsc_polynomial.h"
> > > > > >
> > > > > > +#include <assert.h>
> > > > > > +
> > > > > >  #include <libcamera/base/log.h>
> > > > > >
> > > > > >  /**
> > > > > > @@ -21,7 +23,7 @@ LOG_DEFINE_CATEGORY(LscPolynomial)
> > > > > >  namespace ipa {
> > > > > >
> > > > > >  /**
> > > > > > - * \class LscPolynomial
> > > > > > + * \class Polynomial
> > > > > >   * \brief Class for handling even polynomials used in lens shading correction
> > > > > >   *
> > > > > >   * Shading artifacts of camera lenses can be modeled using even radial
> > > > > > @@ -31,7 +33,7 @@ namespace ipa {
> > > > > >   */
> > > > > >
> > > > > >  /**
> > > > > > - * \fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > > > > > + * \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > > > > >                       double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> > > > > >                       double k4 = 0.0)
> > > > > >   * \brief Construct a polynomial using the given coefficients
> > > > > > @@ -45,7 +47,13 @@ namespace ipa {
> > > > > >   */
> > > > > >
> > > > > >  /**
> > > > > > - * \fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)
> > > > > > + * \fn Polynomial::Polynomial(const Polynomial &other)
> > > > > > + * \brief Construct a Polynomial by copy
> > > > > > + * \param[in] other The Polynomial to copy construct from
> > > > > > + */
> > > > > > +
> > > > > > +/**
> > > > > > + * \fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)
> > > > > >   * \brief Sample the polynomial at the given normalized pixel position
> > > > > >   *
> > > > > >   * This functions samples the polynomial at the given pixel position divided by
> > > > > > @@ -55,9 +63,21 @@ namespace ipa {
> > > > > >   * \param y y position in normalized coordinates
> > > > > >   * \return The sampled value
> > > > > >   */
> > > > > > +double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
> > > > > > +{
> > > > > > +       double dx = x - cnx_;
> > > > > > +       double dy = y - cny_;
> > > > > > +       double r = sqrt(dx * dx + dy * dy);
> > > > > > +       double res = 1.0;
> > > > > > +
> > > > > > +       for (unsigned int i = 0; i < coefficients_.size(); i++)
> > > > > > +               res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> > > > > > +
> > > > > > +       return res;
> > > > > > +}
> > > > >
> > > > > These are plain refactorings. I see that the whole libipa rework
> > > > > is difficult to do in small patches, but I think it would make review
> > > > > (and later history reading) easier if we'd cut out these refactorings
> > > > > into separate preparatory patches.
> > > >
> > > > I think we all knew that this work was going to be hard to review as
> > > > it was hard to implement.
> > > >
> > > > I again suggest to look at the final result more than the diff.
> > >
> > > :-(
> > >
> > > > I could break the patches down in small non-compilable units if it
> > > > helps.
>
> I'm not sure if non-compilable patches really help. But I think there
> are logical units that could be cut out (Like going from members in the
> components to a map) in a compilable way. I feel your pain and am at the
> same time a bit concerned that reading the history will be a pain if we
> do everything in one big step.
>

I'll see what I can do then

> Best regards,
> Stefan
>
> > >
> > > It will help review, if those smaller patches are self-contained and can
> > > be reviewed on their own, even if they don't necessarily compile on
> > > their own.
> > >
> > > > > >
> > > > > >  /**
> > > > > > - * \fn LscPolynomial::getM()
> > > > > > + * \fn Polynomial::getM()
> > > > > >   * \brief Get the value m as described in the dng specification
> > > > > >   *
> > > > > >   * Returns m according to dng spec. m represents the Euclidean distance
> > >
> > > [snip]
> > >
> > > > > > diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
> > > > > > index 0a256e225327..7c656d95acfb 100644
> > > > > > --- a/src/ipa/rkisp1/algorithms/lsc.h
> > > > > > +++ b/src/ipa/rkisp1/algorithms/lsc.h
> > > > > > @@ -2,17 +2,23 @@
> > > > > >  /*
> > > > > >   * Copyright (C) 2021-2022, Ideas On Board
> > > > > >   *
> > > > > > - * RkISP1 Lens Shading Correction control
> > > > > > + * RkISP1 Lens Shading Correction algorithm
> > > > > >   */
> > > > > >
> > > > > >  #pragma once
> > > > > >
> > > > > > -#include <map>
> > > > > > -#include <memory>
> > > > > > +#include <vector>
> > > > > >
> > > > > > -#include "libipa/interpolator.h"
> > > > > > +#include <linux/rkisp1-config.h>
> > > > > > +
> > > > > > +#include "libcamera/internal/value_node.h"
> > > > > > +
> > > > > > +#include "libipa/fixedpoint.h"
> > > > > > +#include "libipa/lsc.h"
> > > > > >
> > > > > >  #include "algorithm.h"
> > > > > > +#include "ipa_context.h"
> > > > > > +#include "params.h"
> > > > > >
> > > > > >  namespace libcamera {
> > > > > >
> > > > > > @@ -37,39 +43,28 @@ public:
> > > > > >                      const rkisp1_stat_buffer *stats,
> > > > > >                      ControlList &metadata) override;
> > > > > >
> > > > > > -       struct Components {
> > > > > > -               std::vector<uint16_t> r;
> > > > > > -               std::vector<uint16_t> gr;
> > > > > > -               std::vector<uint16_t> gb;
> > > > > > -               std::vector<uint16_t> b;
> > > > > > -       };
> > > > > > -
> > > > > > -       class ShadingDescriptor
> > > > > > -       {
> > > > > > -       public:
> > > > > > -               virtual ~ShadingDescriptor() = default;
> > > > > > -               virtual Components sampleForCrop(const Rectangle &cropRectangle,
> > > > > > -                                                Span<const double> xSizes,
> > > > > > -                                                Span<const double> ySizes) = 0;
> > > > > > -       };
> > > > > > -
> > > > > > -       using ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;
> > > > > > -
> > > > > >  private:
> > > > > > +       std::vector<double> parseSizes(const ValueNode &tuningData,
> > > > > > +                                      const char *prop);
> > > > > > +       std::vector<double> sizesToPositions(Span<const double> sizes);
> > > > > > +
> > > > > >         void setParameters(rkisp1_cif_isp_lsc_config &config);
> > > > > > -       void copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);
> > > > > > +       void copyTable(rkisp1_cif_isp_lsc_config &config,
> > > > > > +                      const ipa::lsc::Components<uint16_t> &set0);
> > > > > >
> > > > > > -       ShadingDescriptorMap shadingDescriptors_;
> > > > > > -       ipa::Interpolator<Components> sets_;
> > > > > >         std::vector<double> xSize_;
> > > > > >         std::vector<double> ySize_;
> > > > > >         uint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > >         uint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > >         uint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > >         uint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > > +       std::vector<double> xPos_;
> > > > > > +       std::vector<double> yPos_;
> > > > > >
> > > > > >         unsigned int lastAppliedCt_;
> > > > > >         unsigned int lastAppliedQuantizedCt_;
> > > > > > +
> > > > > > +       LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;
> > > > >
> > > > > I don't really like specifying the fixed format at compile time, as we
> > > > > will have a hard time to implement support for different formats at
> > > > > runtime like Mali supports. But I also don't see an easy way out of
> > > > > that.
> > > > >
> > > > > >  };
> > > > > >
> > > > > >  } /* namespace ipa::rkisp1::algorithms */
> > > > > > diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
> > > > > > index cd97e10bcf2b..005f4102b4f6 100644
> > > > > > --- a/src/ipa/rkisp1/ipa_context.h
> > > > > > +++ b/src/ipa/rkisp1/ipa_context.h
> > > > > > @@ -30,6 +30,7 @@
> > > > > >  #include "libipa/ccm.h"
> > > > > >  #include "libipa/fc_queue.h"
> > > > > >  #include "libipa/fixedpoint.h"
> > > > > > +#include "libipa/lsc.h"
> > > > > >
> > > > > >  namespace libcamera {
> > > > > >
> > > > > > @@ -138,9 +139,7 @@ struct IPAActiveState {
> > > > > >                 double strength;
> > > > > >         } wdr;
> > > > > >
> > > > > > -       struct {
> > > > > > -               bool enabled;
> > > > > > -       } lsc;
> > > > > > +       ipa::lsc::ActiveState lsc;
> > > > >
> > > > > That will really ease IPA development.
> > > > >
> > > > > >  };
> > > > > >
> > > > > >  struct IPAFrameContext : public FrameContext {
> > > > > > @@ -213,10 +212,7 @@ struct IPAFrameContext : public FrameContext {
> > > > > >                 double gain;
> > > > > >         } wdr;
> > > > > >
> > > > > > -       struct {
> > > > > > -               bool enabled;
> > > > > > -               bool update;
> > > > > > -       } lsc;
> > > > > > +       ipa::lsc::FrameContext lsc;
> > > > > >  };
> > > > >
> > > > > This is a really massive patch and I wonder if it would be possible to
> > > > > split it out into multiple changes (also in case we have regressions). I
> > > > > know this is more work, but otoh only rkisp1 uses that. So it would be
> > > > > possible?
> > > >
> > > > Everything is possible.
> > > >
> > > > I guess we'll miss the next release though.
> > >
> > > Don't worry about releases. They are just numbers. Let's move forward
> > > with this work, give it review priority, and merge it when it will be
> > > ready.
> > >
> > > > > >  struct IPAContext {
> > >
> > > --
> > > Regards,
> > >
> > > Laurent Pinchart
Stefan Klug June 29, 2026, 11:24 a.m. UTC | #12
Quoting Jacopo Mondi (2026-06-29 11:09:55)
> On Mon, Jun 29, 2026 at 10:04:31AM +0200, Stefan Klug wrote:
> > Hi Jacopo,
> >
> > Quoting Jacopo Mondi (2026-06-27 11:24:21)
> > > Hi Laurent
> > >
> > > On Fri, Jun 26, 2026 at 10:51:51PM +0300, Laurent Pinchart wrote:
> > > > On Fri, Jun 26, 2026 at 04:26:53PM +0200, Jacopo Mondi wrote:
> > > > > On Fri, Jun 26, 2026 at 11:18:12AM +0200, Stefan Klug wrote:
> > > > > > Quoting Jacopo Mondi (2026-06-15 16:05:34)
> > > > > > > Introduce an Lsc algorithm implementation in libIPA.
> > > > > > >
> > > > > > > The LscAlgorithm class supports two algorithms: grid-based lsc and
> > > > > > > polynomial-based lsc.
> > > > > > >
> > > > > > > Both implementations have been generalized from the existing ones in the
> > > > > > > RkISP1 IPA.
> > > > > > >
> > > > > > > Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> > > > > > > ---
> > > > > > >  src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++
> > > > > > >  src/ipa/libipa/lsc.h              | 145 +++++++++++++
> > > > > > >  src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++
> > > > > > >  src/ipa/libipa/lsc_base.h         |  59 ++++++
> > > > > > >  src/ipa/libipa/lsc_grid.cpp       |  33 +++
> > > > > > >  src/ipa/libipa/lsc_grid.h         | 113 ++++++++++
> > > > > > >  src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-
> > > > > > >  src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----
> > > > > > >  src/ipa/libipa/meson.build        |   5 +
> > > > > > >  src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------
> > > > > > >  src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--
> > > > > > >  src/ipa/rkisp1/ipa_context.h      |  10 +-
> > > > > > >  12 files changed, 1126 insertions(+), 450 deletions(-)
> > > > > > >
> > > > > > > diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
> > > > > > > new file mode 100644
> > > > > > > index 000000000000..ae713a776778
> > > > > > > --- /dev/null
> > > > > > > +++ b/src/ipa/libipa/lsc.cpp
> > > > > > > @@ -0,0 +1,322 @@
> > > >
> > > > [snip]
> > > >
> > > > > > > +/**
> > > > > > > + * \class LscAlgorithm
> > > > > > > + * \brief libIPA lsc algorithm implementation
> > > > > > > + * \tparam T The type used to store the gain values when loaded from tuning file
> > > > > > > + * \tparam U The fixedpoint lsc engine register format
> > > > > > > + *
> > > > > > > + * Due to the optical characteristics of the lens, the light intensity received
> > > > > > > + * by the sensor is not uniform. The Lens Shading Correction algorithm applies
> > > > > > > + * multipliers to all pixels to compensate for the lens shading effect.
> > > > > > > + *
> > > > > > > + * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.
> > > > > > > + * It provides support for parsing the tuning file content and generate tables
> > > > > > > + * of per-colour temperature gains that IPA algorithms can use to program their
> > > > > > > + * lsc engine.
> > > > > > > + *
> > > > > > > + * The init() function parses the tuning file and loads the gain tables either
> > > > > > > + * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain
> > > > > > > + * tables are organized per-colour temperature with per-colour components gain
> > > > > > > + * vectors or polynomial coefficients. The colour components names are
> > > > > > > + * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc
> > > > > > > + * engine support 4 colour components (r, gr, gb, b), some only support 3 colour
> > > > > > > + * components (r, g, b). The name (and number) of the expected colour components
> > > > > > > + * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys
> > > > > > > + * field.
> > > > > > > + *
> > > > > > > + * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour
> > > > > > > + * components. The gain table has been here omitted, but the expected number
> > > > > >
> > > > > > Nit: s/here omitted/omitted here/
> > > > >
> > > > > I think 'here omitted' is correct
> > > >
> > > > "omitted here" sounds more correct.
> > > >
> > > > > > > + * of entries has be equal to
> > > > > >
> > > > > > Nit: has to be
> > > > > >
> > > > > > > + * LscDescriptor::numHCells * LscDescriptor::numVCells.
> > > > > > > + *
> > > > > > > + * \code{.yaml}
> > > > > > > + * - Lsc:
> > > > > > > + *    sets:
> > > > > > > + *      - ct: 2500
> > > > > > > + *        r: [
> > > > > > > + *             .. gains table omitted..
> > > > > > > + *        ]
> > > > > > > + *        g: [
> > > > > > > + *             .. gains table omitted..
> > > > > > > + *        ]
> > > > > > > + *        b: [
> > > > > > > + *             .. gains table omitted..
> > > > > > > + *        ]
> > > > > > > + *      - ct: 6500
> > > > > > > + *        r: [
> > > > > > > + *             .. gains table omitted..
> > > > > > > + *        ]
> > > > > > > + *        g: [
> > > > > > > + *             .. gains table omitted..
> > > > > > > + *        ]
> > > > > > > + *        b: [
> > > > > > > + *             .. gains table omitted..
> > > > > > > + *        ]
> > > > > > > + * \endcode
> > > > > > > + *
> > > > > > > + * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'
> > > > > > > + * colour components:
> > > > > > > + *
> > > > > > > + * \code{.yaml}
> > > > > > > + * - Lsc:
> > > > > > > + *    type: "polynomial"
> > > > > > > + *    sets:
> > > > > > > + *      - ct: 2500
> > > > > > > + *        r:
> > > > > > > + *          cx: 0.5006571711950275
> > > > > > > + *          cy: 0.510093737499277
> > > > > > > + *          k0: 1.5393282208428813
> > > > > > > + *          k1: -1.1434559757908016
> > > > > > > + *          k2: 4.332602305814554
> > > > > > > + *          k3: 0.0
> > > > > > > + *          k4: 0.0
> > > > > > > + *        gr:
> > > > > > > + *          cx: 0.5009320529087338
> > > > > > > + *          cy: 0.511208038949085
> > > > > > > + *          k0: 1.5634738574805407
> > > > > > > + *          k1: -1.5623484259968348
> > > > > > > + *          k2: 4.846686073656501
> > > > > > > + *          k3: 0.0
> > > > > > > + *          k4: 0.0
> > > > > > > + *        gb:
> > > > > > > + *          cx: 0.5012013290343839
> > > > > > > + *          cy: 0.5128251541578288
> > > > > > > + *          k0: 1.526147944919103
> > > > > > > + *          k1: -1.4316976083689723
> > > > > > > + *          k2: 4.792604063222728
> > > > > > > + *          k3: 0.0
> > > > > > > + *          k4: 0.0
> > > > > > > + *        b:
> > > > > > > + *          cx: 0.49864139511067784
> > > > > > > + *          cy: 0.5162095081739346
> > > > > > > + *          k0: 1.0405245474038738
> > > > > > > + *          k1: 0.05618339879447103
> > > > > > > + *          k2: 1.8792813594001752
> > > > > > > + *          k3: 0.0
> > > > > > > + *          k4: 0.0
> > > > > > > + *      - ct: 6000
> > > > > > > + *        r:
> > > > > > > + *          cx: 0.5006202239353942
> > > > > > > + *          cy: 0.5099531318307661
> > > > > > > + *          k0: 1.4702946023945032
> > > > > > > + *          k1: -0.8893767547927631
> > > > > > > + *          k2: 3.920547732201387
> > > > > > > + *          k3: 0.0
> > > > > > > + *          k4: 0.0
> > > > > > > + *        gr:
> > > > > > > + *          cx: 0.500907874178317
> > > > > > > + *          cy: 0.511084916024106
> > > > > > > + *          k0: 1.5336172760559457
> > > > > > > + *          k1: -1.39964026514435
> > > > > > > + *          k2: 4.565487728954618
> > > > > > > + *          k3: 0.0
> > > > > > > + *          k4: 0.0
> > > > > > > + *        gb:
> > > > > > > + *          cx: 0.5011898608900477
> > > > > > > + *          cy: 0.5126797906745105
> > > > > > > + *          k0: 1.5013145790354843
> > > > > > > + *          k1: -1.2747407173754124
> > > > > > > + *          k2: 4.514682876897286
> > > > > > > + *          k3: 0.0
> > > > > > > + *          k4: 0.0
> > > > > > > + *        b:
> > > > > > > + *          cx: 0.4987561413116136
> > > > > > > + *          cy: 0.5159619420778772
> > > > > > > + *          k0: 1.0102986422191802
> > > > > > > + *          k1: 0.13263449763985727
> > > > > > > + *          k2: 1.686556107316064
> > > > > > > + *          k3: 0.0
> > > > > > > + *          k4: 0.0
> > > > > > > + * \endcode
> > > > > > > + *
> > > > > > > + * The lsc tables or the polynomial definition are generated at tuning time
> > > > > > > + * using an image of known resolution which needs to be specified in
> > > > > > > + * LscDescriptor::sensorSize.
> > > > > > > + *
> > > > > > > + * At LscAlgorithm::configure() time the lsc tables are re-sampled on the
> > > > > > > + * sensor's crop rectangle in use to adapt them to the configuration in use for
> > > > > > > + * a streaming session. Polynomial lsc tables support re-sampling and can be
> > > > > > > + * applied to any sensor configuration. Grid-based lsc tables cannot be
> > > > > > > + * re-sampled and the configuration as parsed from the tuning file is used for
> > > > > > > + * all sensor configurations providing best-effort results.
> > > > > > > + *
> > > > > > > + * When the IPA algorithms wants to get access to the (resampled) tables to
> > > > > > > + * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get
> > > > > > > + * an lsc table interpolated by the LscAlgorithm class for the specified colour
> > > > > > > + * temperature. If the algorithm wants to access the non-interpolated tables it
> > > > > > > + * can retrieve them using LscAlgorithm::getComponents().
> > > > > > > + */
> > > >
> > > > [snip]
> > > >
> > > > > > > diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
> > > > > > > new file mode 100644
> > > > > > > index 000000000000..aecae305aa9c
> > > > > > > --- /dev/null
> > > > > > > +++ b/src/ipa/libipa/lsc_base.h
> > > > > > > @@ -0,0 +1,59 @@
> > > > > > > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > > > > > > +/*
> > > > > > > + * Copyright (C) 2026 Ideas on Board Oy
> > > > > > > + *
> > > > > > > + * Base classes and types for LSC algorithms implementations
> > > > > > > + */
> > > > > > > +
> > > > > > > +#pragma once
> > > > > > > +
> > > > > > > +#include <map>
> > > > > > > +#include <string>
> > > > > > > +#include <vector>
> > > > > > > +
> > > > > > > +#include <libcamera/geometry.h>
> > > > > > > +
> > > > > > > +#include "libcamera/internal/value_node.h"
> > > > > > > +
> > > > > > > +namespace libcamera {
> > > > > > > +
> > > > > > > +namespace ipa {
> > > > > > > +
> > > > > > > +namespace lsc {
> > > > > > > +
> > > > > > > +template<typename T>
> > > > > > > +class Components : public std::map<std::string, std::vector<T>>
> > > > > > > +{
> > > > > >
> > > > > > I'm a bit uneasy here. On the one hand we are going great lengths to
> > > > > > write super optimized code striving for static allocations as much as
> > > > > > possible and now we add a string based map lookup that is run per frame.
> > > > >
> > > > > How are 'string based lookup' and 'static memory allocation' related ?
> > > > >
> > > > > > On the other hand I don't actually know how expensive the lookup is.
> > > > >
> > > > > So, Component is used at init to parse the tuning file. Fine.
> > > > >
> > > > > At configure a set of Components is re-sampled (polynomial) or
> > > > > returned as parsed from config file. Fine.
> > > > >
> > > > > Now, the run-time.
> > > > >
> > > > > I presume you're thinking at (using rkisp1 as an example)
> > > > >
> > > > >  void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
> > > > > -                                     const Components &set)
> > > > > +                                     const ipa::lsc::Components<uint16_t> &set)
> > > > >  {
> > > > > -       std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
> > > > > -       std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
> > > > > -       std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
> > > > > -       std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
> > > > > +       const auto &r = set.at("r");
> > > > > +       std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
> > > > > +       const auto &gr = set.at("gr");
> > > > > +       std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
> > > > > +       const auto &gb = set.at("gb");
> > > > > +       std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
> > > > > +       const auto &b = set.at("b");
> > > > > +       std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
> > > > >  }
> > > > >
> > > > > yes, we do a lookup on a string.
> > > > >
> > > > > The existing implementation stores the Components vectors in a struct
> > > > >
> > > > >     struct Components {
> > > > >             uint32_t ct;
> > > > >             std::vector<uint16_t> r;
> > > > >             std::vector<uint16_t> gr;
> > > > >             std::vector<uint16_t> gb;
> > > > >             std::vector<uint16_t> b;
> > > > >     };
> > > > >
> > > > > This patch instead stores them in a map.
> > > > >
> > > > > Now, not an STL guru here, but I presume that if your only concern is
> > > > > the lookup, a map with 4 elements top shouldn't be that hard.
> > > > >
> > > > > If there are implications I'm missing about storing the vectors in the
> > > > > map instead of storing them in a struct I would be glad to learn about
> > > > > that.
> > > >
> > > > A map with string keys will be significantly less efficient. First the
> > > > string will be hashed, then the hash will be used in the lookup, and
> > >
> > > Are you sure hashing happen for std::map too or is it just for
> > > std::unordered_map ?
> > >
> > > As far as I know, std::map sorts the keys by comparing them and
> > > the default Compare function is std::less<> which for std::string calls
> > > std::string::operator< which has a complexity linear in the string
> > > length (1 or 2 chars). Then the sorted key lookup itself should be
> > > O(log n), which for a map of up to two elements is nothing.
> > >
> > > If using an unordered_map<> then hasing of the key (which [1] doesn't
> > > report the complexity of) will take place, then the lookup should
> > > happen in linear time.
> > >
> > > [1] https://en.cppreference.com/cpp/string/basic_string/hash
> > > > finally the strings will be compared.
> > > >
> > >
> > > When the LSC tables have to be updated because of a significant change
> > > in colour temperature we interpolate vectors of thousands of elements
> > > for the new colour temperature and then memcpy them to the parameters buffer.
> > >
> > > Before reworking anything here I would like to see a comparison of these
> > > costs to the above described complexity of using a map of 4 elements
> > > indexed by a string of 1 or 2 characters.
> > >
> > > And no, we're not:
> > >
> > > > > > going great lengths to
> > > > > > write super optimized code striving for static allocations as much as
> > > > > > possible
> > >
> > > Both when loading from yaml file and when re-sampling the polynomial
> > > the destination vectors are dynamically allocatated. This is good, it
> > > avoids relocations, but it's certainly not "static allocation as much
> > > as possible" and I don't think anyone complained.
> >
> > I feel like I need to clarify my initial words. I'm well aware that a
> > vector is dynamically allocating memory. The thing is that I felt bad
> > for even having these vectors in the frame context because there is this
> > "we'd eventually like to do everything static" mantra in my head. With
> > the "great lengths" I thought more of the "super optimized" aspect. With
> > static allocation I think more of things like the fixed number of entries
> > in the FCQueue or the redesigned metadata with preallocated fixed memory
> > buffers. I'm a big fan of keep it simple and let the language do it's
> > thing. That is why camshark is full of maps and string lookups.
> >
> > Fixed FCQueue makes sense as it prevents reallocation during streaming.
> > The Yaml parsing time
> >
> 
> Ok.
> 
> How is any of this related to this patch and the string lookup though ?
> 
> > >
> > > > > > I was wondering if it is possible to template that using a static set as
> > > > > > temüplate param and a fixed size array as container but then you can't
> > > > > > configure that at init() time and end up with too much template
> > > > > > magic....no
> > > > > >
> > > > > > As a compromise, would it make sense to index the map by an enum, so
> > > > > > that we could define an enum for the possible types (it will most likely
> > > > > > ever only be g,r,b,rg,bg (possible ir in the future). That would at
> > > > > > least replace the string lookup by an int lookup, and we'd only have
> > > > > > strings at yaml parse time?
> > > >
> > > > If we have an index, can't we just make it an array or vector ?
> > > >
> > >
> > > Each IPA has different indexes that reflect the number of tables the
> > > hadware supprots ('r' 'g' 'b' for mali, 'r' 'gr' 'gb' 'b' for rkisp1
> > > etc). The current implementation allows the IPA to specify which keys
> > > to use to parse the tables (or polynomials) from the tuning file and
> > > then use the same keys for tables lookup when LSC have to be updated.
> > >
> > > This is of course the most flexible solution. We can provide a less
> > > flexible interface to IPAs and assume up to 4 vectors are supported
> > > and let IPA map the array entries to the colour component.
> > >
> > > Again, this seems largely unecessary to me considering the above
> > > described cost of using a map is justified by a nicer API.
> >
> > I still think, that replacing the strings with an enum would keep the
> > same flexibility, add a bit more compile time checking and maybe would
> > even be a tad more efficient - but as you wrote that gain will be
> > neglectable.
> >
> 
> Yes, neglectable, even more so considering what happens after the
> lookup (interpolation and mem copies).
> 
> I need to pass the string keys to parse the tuning file in at init()
> time anyway and the IPA using the same identifiers for the lookup is
> way nicer as an API instead of having to mix strings and enumerated
> types.

Why is that? You'd pass the list of enum entries in at init() time. This
way the enum to string translation is all hidden inside libipa. The
strings wouldn't be part of the API at all.

> 
> For the compile time checking, I don't think this is a concern. We're
> not talking of application provided input or application facing APIs,
> we're talking about in-tree IPA code, it's not like you can write and
> run an IPA and get a key identifier wrong. You'll notice quite soon I
> guess (and in case of a wrong tuning file, you'll fail at init() time
> anyway, so you'll never get to do a lookup).

This is also the API that shall be used by others to implement their
IPAs (not a super common usecase though). Agreed, we'll likely notice.
But what is the use case of having free form strings there?

Regards,
Stefan

> 
> Honestly, I would like to see more compelling arguments for changing
> this implementation before doing it.
> 
> > >
> > > > > So is the only concern really comparing strings vs comparing integers
> > > > > when looking up a key ? In the same code path that memcpys thousands
> > > > > of values to program the lsc tables ?
> > > > >
> > > > > I feel like I'm missing something.
> > > > >
> > > > > > > +};
> > > > > > > +
> > > > > > > +template<typename T>
> > > > > > > +class ComponentsMap : public std::map<unsigned int, Components<T>>
> > > > > > > +{
> > > > > > > +};
> > > > > > > +
> > > > > > > +} /* namespace lsc */
> > > > > > > +
> > > > > > > +struct LscDescriptor {
> > > > > > > +       std::vector<std::string> keys;
> > > > > > > +       unsigned int numHCells;
> > > > > > > +       unsigned int numVCells;
> > > > > > > +       Size sensorSize;
> > > > > > > +};
> > > > > > > +
> > > > > > > +template<typename T, typename U>
> > > > > > > +class LscImplementation
> > > > > >
> > > > > > On v1 Dan commented that U is not used in grid based. But it is used in
> > > > > > the polynomial case. What about dropping T and defining it as
> > > > > > U::QuantizedType? That would work for both cases and you get rid of one
> > > > > > parameter. (And if there is a hardware that has a different register
> > > > > > size than U::QuantizedType, the current solution would rely on luck,
> > > > > > too.)
> > > > >
> > > > > This one is a good suggestion.
> > > > >
> > > > > Right now the template argument T serves only for loading values from
> > > > > file in a type large enough to accommodate them
> > > > >
> > > > > Using U::QuantizedType seems like a good idea.
> > > > > We're gonna drop it anyway an move to store and load float gains from
> > > > > the tuning file, but in the interim using Q::QuantizedType seems good
> > > > > enough.
> > > > >
> > > > > > > +{
> > > > > > > +public:
> > > > > > > +       virtual ~LscImplementation() = default;
> > > > > > > +
> > > > > > > +       virtual int parseLscData(const ValueNode &tuningData,
> > > > > > > +                                const LscDescriptor &descriptor) = 0;
> > > > > > > +
> > > > > > > +       virtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> > > > > > > +                                                     const std::vector<double> &xPos,
> > > > > > > +                                                     const std::vector<double> &yPos) = 0;
> > > > > > > +};
> > > > > > > +
> > > > > > > +} /* namespace ipa */
> > > > > > > +
> > > > > > > +} /* namespace libcamera */
> > > >
> > > > [snip]
> > > >
> > > > > > > diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
> > > > > > > index f607d86c54c3..8d3f1883d6a5 100644
> > > > > > > --- a/src/ipa/libipa/lsc_polynomial.cpp
> > > > > > > +++ b/src/ipa/libipa/lsc_polynomial.cpp
> > > > > > > @@ -1,12 +1,14 @@
> > > > > > >  /* SPDX-License-Identifier: LGPL-2.1-or-later */
> > > > > > >  /*
> > > > > > > - * Copyright (C) 2024, Ideas On Board
> > > > > > > + * Copyright (C) 2026, Ideas On Board
> > > > > > >   *
> > > > > > > - * Polynomial class to represent lens shading correction
> > > > > > > + * Polynomial based lens shading correction
> > > > > > >   */
> > > > > > >
> > > > > > >  #include "lsc_polynomial.h"
> > > > > > >
> > > > > > > +#include <assert.h>
> > > > > > > +
> > > > > > >  #include <libcamera/base/log.h>
> > > > > > >
> > > > > > >  /**
> > > > > > > @@ -21,7 +23,7 @@ LOG_DEFINE_CATEGORY(LscPolynomial)
> > > > > > >  namespace ipa {
> > > > > > >
> > > > > > >  /**
> > > > > > > - * \class LscPolynomial
> > > > > > > + * \class Polynomial
> > > > > > >   * \brief Class for handling even polynomials used in lens shading correction
> > > > > > >   *
> > > > > > >   * Shading artifacts of camera lenses can be modeled using even radial
> > > > > > > @@ -31,7 +33,7 @@ namespace ipa {
> > > > > > >   */
> > > > > > >
> > > > > > >  /**
> > > > > > > - * \fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > > > > > > + * \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > > > > > >                       double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> > > > > > >                       double k4 = 0.0)
> > > > > > >   * \brief Construct a polynomial using the given coefficients
> > > > > > > @@ -45,7 +47,13 @@ namespace ipa {
> > > > > > >   */
> > > > > > >
> > > > > > >  /**
> > > > > > > - * \fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)
> > > > > > > + * \fn Polynomial::Polynomial(const Polynomial &other)
> > > > > > > + * \brief Construct a Polynomial by copy
> > > > > > > + * \param[in] other The Polynomial to copy construct from
> > > > > > > + */
> > > > > > > +
> > > > > > > +/**
> > > > > > > + * \fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)
> > > > > > >   * \brief Sample the polynomial at the given normalized pixel position
> > > > > > >   *
> > > > > > >   * This functions samples the polynomial at the given pixel position divided by
> > > > > > > @@ -55,9 +63,21 @@ namespace ipa {
> > > > > > >   * \param y y position in normalized coordinates
> > > > > > >   * \return The sampled value
> > > > > > >   */
> > > > > > > +double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
> > > > > > > +{
> > > > > > > +       double dx = x - cnx_;
> > > > > > > +       double dy = y - cny_;
> > > > > > > +       double r = sqrt(dx * dx + dy * dy);
> > > > > > > +       double res = 1.0;
> > > > > > > +
> > > > > > > +       for (unsigned int i = 0; i < coefficients_.size(); i++)
> > > > > > > +               res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> > > > > > > +
> > > > > > > +       return res;
> > > > > > > +}
> > > > > >
> > > > > > These are plain refactorings. I see that the whole libipa rework
> > > > > > is difficult to do in small patches, but I think it would make review
> > > > > > (and later history reading) easier if we'd cut out these refactorings
> > > > > > into separate preparatory patches.
> > > > >
> > > > > I think we all knew that this work was going to be hard to review as
> > > > > it was hard to implement.
> > > > >
> > > > > I again suggest to look at the final result more than the diff.
> > > >
> > > > :-(
> > > >
> > > > > I could break the patches down in small non-compilable units if it
> > > > > helps.
> >
> > I'm not sure if non-compilable patches really help. But I think there
> > are logical units that could be cut out (Like going from members in the
> > components to a map) in a compilable way. I feel your pain and am at the
> > same time a bit concerned that reading the history will be a pain if we
> > do everything in one big step.
> >
> 
> I'll see what I can do then
> 
> > Best regards,
> > Stefan
> >
> > > >
> > > > It will help review, if those smaller patches are self-contained and can
> > > > be reviewed on their own, even if they don't necessarily compile on
> > > > their own.
> > > >
> > > > > > >
> > > > > > >  /**
> > > > > > > - * \fn LscPolynomial::getM()
> > > > > > > + * \fn Polynomial::getM()
> > > > > > >   * \brief Get the value m as described in the dng specification
> > > > > > >   *
> > > > > > >   * Returns m according to dng spec. m represents the Euclidean distance
> > > >
> > > > [snip]
> > > >
> > > > > > > diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
> > > > > > > index 0a256e225327..7c656d95acfb 100644
> > > > > > > --- a/src/ipa/rkisp1/algorithms/lsc.h
> > > > > > > +++ b/src/ipa/rkisp1/algorithms/lsc.h
> > > > > > > @@ -2,17 +2,23 @@
> > > > > > >  /*
> > > > > > >   * Copyright (C) 2021-2022, Ideas On Board
> > > > > > >   *
> > > > > > > - * RkISP1 Lens Shading Correction control
> > > > > > > + * RkISP1 Lens Shading Correction algorithm
> > > > > > >   */
> > > > > > >
> > > > > > >  #pragma once
> > > > > > >
> > > > > > > -#include <map>
> > > > > > > -#include <memory>
> > > > > > > +#include <vector>
> > > > > > >
> > > > > > > -#include "libipa/interpolator.h"
> > > > > > > +#include <linux/rkisp1-config.h>
> > > > > > > +
> > > > > > > +#include "libcamera/internal/value_node.h"
> > > > > > > +
> > > > > > > +#include "libipa/fixedpoint.h"
> > > > > > > +#include "libipa/lsc.h"
> > > > > > >
> > > > > > >  #include "algorithm.h"
> > > > > > > +#include "ipa_context.h"
> > > > > > > +#include "params.h"
> > > > > > >
> > > > > > >  namespace libcamera {
> > > > > > >
> > > > > > > @@ -37,39 +43,28 @@ public:
> > > > > > >                      const rkisp1_stat_buffer *stats,
> > > > > > >                      ControlList &metadata) override;
> > > > > > >
> > > > > > > -       struct Components {
> > > > > > > -               std::vector<uint16_t> r;
> > > > > > > -               std::vector<uint16_t> gr;
> > > > > > > -               std::vector<uint16_t> gb;
> > > > > > > -               std::vector<uint16_t> b;
> > > > > > > -       };
> > > > > > > -
> > > > > > > -       class ShadingDescriptor
> > > > > > > -       {
> > > > > > > -       public:
> > > > > > > -               virtual ~ShadingDescriptor() = default;
> > > > > > > -               virtual Components sampleForCrop(const Rectangle &cropRectangle,
> > > > > > > -                                                Span<const double> xSizes,
> > > > > > > -                                                Span<const double> ySizes) = 0;
> > > > > > > -       };
> > > > > > > -
> > > > > > > -       using ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;
> > > > > > > -
> > > > > > >  private:
> > > > > > > +       std::vector<double> parseSizes(const ValueNode &tuningData,
> > > > > > > +                                      const char *prop);
> > > > > > > +       std::vector<double> sizesToPositions(Span<const double> sizes);
> > > > > > > +
> > > > > > >         void setParameters(rkisp1_cif_isp_lsc_config &config);
> > > > > > > -       void copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);
> > > > > > > +       void copyTable(rkisp1_cif_isp_lsc_config &config,
> > > > > > > +                      const ipa::lsc::Components<uint16_t> &set0);
> > > > > > >
> > > > > > > -       ShadingDescriptorMap shadingDescriptors_;
> > > > > > > -       ipa::Interpolator<Components> sets_;
> > > > > > >         std::vector<double> xSize_;
> > > > > > >         std::vector<double> ySize_;
> > > > > > >         uint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > > >         uint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > > >         uint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > > >         uint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > > > +       std::vector<double> xPos_;
> > > > > > > +       std::vector<double> yPos_;
> > > > > > >
> > > > > > >         unsigned int lastAppliedCt_;
> > > > > > >         unsigned int lastAppliedQuantizedCt_;
> > > > > > > +
> > > > > > > +       LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;
> > > > > >
> > > > > > I don't really like specifying the fixed format at compile time, as we
> > > > > > will have a hard time to implement support for different formats at
> > > > > > runtime like Mali supports. But I also don't see an easy way out of
> > > > > > that.
> > > > > >
> > > > > > >  };
> > > > > > >
> > > > > > >  } /* namespace ipa::rkisp1::algorithms */
> > > > > > > diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
> > > > > > > index cd97e10bcf2b..005f4102b4f6 100644
> > > > > > > --- a/src/ipa/rkisp1/ipa_context.h
> > > > > > > +++ b/src/ipa/rkisp1/ipa_context.h
> > > > > > > @@ -30,6 +30,7 @@
> > > > > > >  #include "libipa/ccm.h"
> > > > > > >  #include "libipa/fc_queue.h"
> > > > > > >  #include "libipa/fixedpoint.h"
> > > > > > > +#include "libipa/lsc.h"
> > > > > > >
> > > > > > >  namespace libcamera {
> > > > > > >
> > > > > > > @@ -138,9 +139,7 @@ struct IPAActiveState {
> > > > > > >                 double strength;
> > > > > > >         } wdr;
> > > > > > >
> > > > > > > -       struct {
> > > > > > > -               bool enabled;
> > > > > > > -       } lsc;
> > > > > > > +       ipa::lsc::ActiveState lsc;
> > > > > >
> > > > > > That will really ease IPA development.
> > > > > >
> > > > > > >  };
> > > > > > >
> > > > > > >  struct IPAFrameContext : public FrameContext {
> > > > > > > @@ -213,10 +212,7 @@ struct IPAFrameContext : public FrameContext {
> > > > > > >                 double gain;
> > > > > > >         } wdr;
> > > > > > >
> > > > > > > -       struct {
> > > > > > > -               bool enabled;
> > > > > > > -               bool update;
> > > > > > > -       } lsc;
> > > > > > > +       ipa::lsc::FrameContext lsc;
> > > > > > >  };
> > > > > >
> > > > > > This is a really massive patch and I wonder if it would be possible to
> > > > > > split it out into multiple changes (also in case we have regressions). I
> > > > > > know this is more work, but otoh only rkisp1 uses that. So it would be
> > > > > > possible?
> > > > >
> > > > > Everything is possible.
> > > > >
> > > > > I guess we'll miss the next release though.
> > > >
> > > > Don't worry about releases. They are just numbers. Let's move forward
> > > > with this work, give it review priority, and merge it when it will be
> > > > ready.
> > > >
> > > > > > >  struct IPAContext {
> > > >
> > > > --
> > > > Regards,
> > > >
> > > > Laurent Pinchart
Jacopo Mondi June 29, 2026, 11:38 a.m. UTC | #13
On Mon, Jun 29, 2026 at 01:24:45PM +0200, Stefan Klug wrote:
> Quoting Jacopo Mondi (2026-06-29 11:09:55)
> > On Mon, Jun 29, 2026 at 10:04:31AM +0200, Stefan Klug wrote:
> > > Hi Jacopo,
> > >
> > > Quoting Jacopo Mondi (2026-06-27 11:24:21)
> > > > Hi Laurent
> > > >
> > > > On Fri, Jun 26, 2026 at 10:51:51PM +0300, Laurent Pinchart wrote:
> > > > > On Fri, Jun 26, 2026 at 04:26:53PM +0200, Jacopo Mondi wrote:
> > > > > > On Fri, Jun 26, 2026 at 11:18:12AM +0200, Stefan Klug wrote:
> > > > > > > Quoting Jacopo Mondi (2026-06-15 16:05:34)
> > > > > > > > Introduce an Lsc algorithm implementation in libIPA.
> > > > > > > >
> > > > > > > > The LscAlgorithm class supports two algorithms: grid-based lsc and
> > > > > > > > polynomial-based lsc.
> > > > > > > >
> > > > > > > > Both implementations have been generalized from the existing ones in the
> > > > > > > > RkISP1 IPA.
> > > > > > > >
> > > > > > > > Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> > > > > > > > ---
> > > > > > > >  src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++
> > > > > > > >  src/ipa/libipa/lsc.h              | 145 +++++++++++++
> > > > > > > >  src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++
> > > > > > > >  src/ipa/libipa/lsc_base.h         |  59 ++++++
> > > > > > > >  src/ipa/libipa/lsc_grid.cpp       |  33 +++
> > > > > > > >  src/ipa/libipa/lsc_grid.h         | 113 ++++++++++
> > > > > > > >  src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-
> > > > > > > >  src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----
> > > > > > > >  src/ipa/libipa/meson.build        |   5 +
> > > > > > > >  src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------
> > > > > > > >  src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--
> > > > > > > >  src/ipa/rkisp1/ipa_context.h      |  10 +-
> > > > > > > >  12 files changed, 1126 insertions(+), 450 deletions(-)
> > > > > > > >
> > > > > > > > diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
> > > > > > > > new file mode 100644
> > > > > > > > index 000000000000..ae713a776778
> > > > > > > > --- /dev/null
> > > > > > > > +++ b/src/ipa/libipa/lsc.cpp
> > > > > > > > @@ -0,0 +1,322 @@
> > > > >
> > > > > [snip]
> > > > >
> > > > > > > > +/**
> > > > > > > > + * \class LscAlgorithm
> > > > > > > > + * \brief libIPA lsc algorithm implementation
> > > > > > > > + * \tparam T The type used to store the gain values when loaded from tuning file
> > > > > > > > + * \tparam U The fixedpoint lsc engine register format
> > > > > > > > + *
> > > > > > > > + * Due to the optical characteristics of the lens, the light intensity received
> > > > > > > > + * by the sensor is not uniform. The Lens Shading Correction algorithm applies
> > > > > > > > + * multipliers to all pixels to compensate for the lens shading effect.
> > > > > > > > + *
> > > > > > > > + * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.
> > > > > > > > + * It provides support for parsing the tuning file content and generate tables
> > > > > > > > + * of per-colour temperature gains that IPA algorithms can use to program their
> > > > > > > > + * lsc engine.
> > > > > > > > + *
> > > > > > > > + * The init() function parses the tuning file and loads the gain tables either
> > > > > > > > + * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain
> > > > > > > > + * tables are organized per-colour temperature with per-colour components gain
> > > > > > > > + * vectors or polynomial coefficients. The colour components names are
> > > > > > > > + * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc
> > > > > > > > + * engine support 4 colour components (r, gr, gb, b), some only support 3 colour
> > > > > > > > + * components (r, g, b). The name (and number) of the expected colour components
> > > > > > > > + * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys
> > > > > > > > + * field.
> > > > > > > > + *
> > > > > > > > + * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour
> > > > > > > > + * components. The gain table has been here omitted, but the expected number
> > > > > > >
> > > > > > > Nit: s/here omitted/omitted here/
> > > > > >
> > > > > > I think 'here omitted' is correct
> > > > >
> > > > > "omitted here" sounds more correct.
> > > > >
> > > > > > > > + * of entries has be equal to
> > > > > > >
> > > > > > > Nit: has to be
> > > > > > >
> > > > > > > > + * LscDescriptor::numHCells * LscDescriptor::numVCells.
> > > > > > > > + *
> > > > > > > > + * \code{.yaml}
> > > > > > > > + * - Lsc:
> > > > > > > > + *    sets:
> > > > > > > > + *      - ct: 2500
> > > > > > > > + *        r: [
> > > > > > > > + *             .. gains table omitted..
> > > > > > > > + *        ]
> > > > > > > > + *        g: [
> > > > > > > > + *             .. gains table omitted..
> > > > > > > > + *        ]
> > > > > > > > + *        b: [
> > > > > > > > + *             .. gains table omitted..
> > > > > > > > + *        ]
> > > > > > > > + *      - ct: 6500
> > > > > > > > + *        r: [
> > > > > > > > + *             .. gains table omitted..
> > > > > > > > + *        ]
> > > > > > > > + *        g: [
> > > > > > > > + *             .. gains table omitted..
> > > > > > > > + *        ]
> > > > > > > > + *        b: [
> > > > > > > > + *             .. gains table omitted..
> > > > > > > > + *        ]
> > > > > > > > + * \endcode
> > > > > > > > + *
> > > > > > > > + * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'
> > > > > > > > + * colour components:
> > > > > > > > + *
> > > > > > > > + * \code{.yaml}
> > > > > > > > + * - Lsc:
> > > > > > > > + *    type: "polynomial"
> > > > > > > > + *    sets:
> > > > > > > > + *      - ct: 2500
> > > > > > > > + *        r:
> > > > > > > > + *          cx: 0.5006571711950275
> > > > > > > > + *          cy: 0.510093737499277
> > > > > > > > + *          k0: 1.5393282208428813
> > > > > > > > + *          k1: -1.1434559757908016
> > > > > > > > + *          k2: 4.332602305814554
> > > > > > > > + *          k3: 0.0
> > > > > > > > + *          k4: 0.0
> > > > > > > > + *        gr:
> > > > > > > > + *          cx: 0.5009320529087338
> > > > > > > > + *          cy: 0.511208038949085
> > > > > > > > + *          k0: 1.5634738574805407
> > > > > > > > + *          k1: -1.5623484259968348
> > > > > > > > + *          k2: 4.846686073656501
> > > > > > > > + *          k3: 0.0
> > > > > > > > + *          k4: 0.0
> > > > > > > > + *        gb:
> > > > > > > > + *          cx: 0.5012013290343839
> > > > > > > > + *          cy: 0.5128251541578288
> > > > > > > > + *          k0: 1.526147944919103
> > > > > > > > + *          k1: -1.4316976083689723
> > > > > > > > + *          k2: 4.792604063222728
> > > > > > > > + *          k3: 0.0
> > > > > > > > + *          k4: 0.0
> > > > > > > > + *        b:
> > > > > > > > + *          cx: 0.49864139511067784
> > > > > > > > + *          cy: 0.5162095081739346
> > > > > > > > + *          k0: 1.0405245474038738
> > > > > > > > + *          k1: 0.05618339879447103
> > > > > > > > + *          k2: 1.8792813594001752
> > > > > > > > + *          k3: 0.0
> > > > > > > > + *          k4: 0.0
> > > > > > > > + *      - ct: 6000
> > > > > > > > + *        r:
> > > > > > > > + *          cx: 0.5006202239353942
> > > > > > > > + *          cy: 0.5099531318307661
> > > > > > > > + *          k0: 1.4702946023945032
> > > > > > > > + *          k1: -0.8893767547927631
> > > > > > > > + *          k2: 3.920547732201387
> > > > > > > > + *          k3: 0.0
> > > > > > > > + *          k4: 0.0
> > > > > > > > + *        gr:
> > > > > > > > + *          cx: 0.500907874178317
> > > > > > > > + *          cy: 0.511084916024106
> > > > > > > > + *          k0: 1.5336172760559457
> > > > > > > > + *          k1: -1.39964026514435
> > > > > > > > + *          k2: 4.565487728954618
> > > > > > > > + *          k3: 0.0
> > > > > > > > + *          k4: 0.0
> > > > > > > > + *        gb:
> > > > > > > > + *          cx: 0.5011898608900477
> > > > > > > > + *          cy: 0.5126797906745105
> > > > > > > > + *          k0: 1.5013145790354843
> > > > > > > > + *          k1: -1.2747407173754124
> > > > > > > > + *          k2: 4.514682876897286
> > > > > > > > + *          k3: 0.0
> > > > > > > > + *          k4: 0.0
> > > > > > > > + *        b:
> > > > > > > > + *          cx: 0.4987561413116136
> > > > > > > > + *          cy: 0.5159619420778772
> > > > > > > > + *          k0: 1.0102986422191802
> > > > > > > > + *          k1: 0.13263449763985727
> > > > > > > > + *          k2: 1.686556107316064
> > > > > > > > + *          k3: 0.0
> > > > > > > > + *          k4: 0.0
> > > > > > > > + * \endcode
> > > > > > > > + *
> > > > > > > > + * The lsc tables or the polynomial definition are generated at tuning time
> > > > > > > > + * using an image of known resolution which needs to be specified in
> > > > > > > > + * LscDescriptor::sensorSize.
> > > > > > > > + *
> > > > > > > > + * At LscAlgorithm::configure() time the lsc tables are re-sampled on the
> > > > > > > > + * sensor's crop rectangle in use to adapt them to the configuration in use for
> > > > > > > > + * a streaming session. Polynomial lsc tables support re-sampling and can be
> > > > > > > > + * applied to any sensor configuration. Grid-based lsc tables cannot be
> > > > > > > > + * re-sampled and the configuration as parsed from the tuning file is used for
> > > > > > > > + * all sensor configurations providing best-effort results.
> > > > > > > > + *
> > > > > > > > + * When the IPA algorithms wants to get access to the (resampled) tables to
> > > > > > > > + * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get
> > > > > > > > + * an lsc table interpolated by the LscAlgorithm class for the specified colour
> > > > > > > > + * temperature. If the algorithm wants to access the non-interpolated tables it
> > > > > > > > + * can retrieve them using LscAlgorithm::getComponents().
> > > > > > > > + */
> > > > >
> > > > > [snip]
> > > > >
> > > > > > > > diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
> > > > > > > > new file mode 100644
> > > > > > > > index 000000000000..aecae305aa9c
> > > > > > > > --- /dev/null
> > > > > > > > +++ b/src/ipa/libipa/lsc_base.h
> > > > > > > > @@ -0,0 +1,59 @@
> > > > > > > > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > > > > > > > +/*
> > > > > > > > + * Copyright (C) 2026 Ideas on Board Oy
> > > > > > > > + *
> > > > > > > > + * Base classes and types for LSC algorithms implementations
> > > > > > > > + */
> > > > > > > > +
> > > > > > > > +#pragma once
> > > > > > > > +
> > > > > > > > +#include <map>
> > > > > > > > +#include <string>
> > > > > > > > +#include <vector>
> > > > > > > > +
> > > > > > > > +#include <libcamera/geometry.h>
> > > > > > > > +
> > > > > > > > +#include "libcamera/internal/value_node.h"
> > > > > > > > +
> > > > > > > > +namespace libcamera {
> > > > > > > > +
> > > > > > > > +namespace ipa {
> > > > > > > > +
> > > > > > > > +namespace lsc {
> > > > > > > > +
> > > > > > > > +template<typename T>
> > > > > > > > +class Components : public std::map<std::string, std::vector<T>>
> > > > > > > > +{
> > > > > > >
> > > > > > > I'm a bit uneasy here. On the one hand we are going great lengths to
> > > > > > > write super optimized code striving for static allocations as much as
> > > > > > > possible and now we add a string based map lookup that is run per frame.
> > > > > >
> > > > > > How are 'string based lookup' and 'static memory allocation' related ?
> > > > > >
> > > > > > > On the other hand I don't actually know how expensive the lookup is.
> > > > > >
> > > > > > So, Component is used at init to parse the tuning file. Fine.
> > > > > >
> > > > > > At configure a set of Components is re-sampled (polynomial) or
> > > > > > returned as parsed from config file. Fine.
> > > > > >
> > > > > > Now, the run-time.
> > > > > >
> > > > > > I presume you're thinking at (using rkisp1 as an example)
> > > > > >
> > > > > >  void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
> > > > > > -                                     const Components &set)
> > > > > > +                                     const ipa::lsc::Components<uint16_t> &set)
> > > > > >  {
> > > > > > -       std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
> > > > > > -       std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
> > > > > > -       std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
> > > > > > -       std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
> > > > > > +       const auto &r = set.at("r");
> > > > > > +       std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
> > > > > > +       const auto &gr = set.at("gr");
> > > > > > +       std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
> > > > > > +       const auto &gb = set.at("gb");
> > > > > > +       std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
> > > > > > +       const auto &b = set.at("b");
> > > > > > +       std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
> > > > > >  }
> > > > > >
> > > > > > yes, we do a lookup on a string.
> > > > > >
> > > > > > The existing implementation stores the Components vectors in a struct
> > > > > >
> > > > > >     struct Components {
> > > > > >             uint32_t ct;
> > > > > >             std::vector<uint16_t> r;
> > > > > >             std::vector<uint16_t> gr;
> > > > > >             std::vector<uint16_t> gb;
> > > > > >             std::vector<uint16_t> b;
> > > > > >     };
> > > > > >
> > > > > > This patch instead stores them in a map.
> > > > > >
> > > > > > Now, not an STL guru here, but I presume that if your only concern is
> > > > > > the lookup, a map with 4 elements top shouldn't be that hard.
> > > > > >
> > > > > > If there are implications I'm missing about storing the vectors in the
> > > > > > map instead of storing them in a struct I would be glad to learn about
> > > > > > that.
> > > > >
> > > > > A map with string keys will be significantly less efficient. First the
> > > > > string will be hashed, then the hash will be used in the lookup, and
> > > >
> > > > Are you sure hashing happen for std::map too or is it just for
> > > > std::unordered_map ?
> > > >
> > > > As far as I know, std::map sorts the keys by comparing them and
> > > > the default Compare function is std::less<> which for std::string calls
> > > > std::string::operator< which has a complexity linear in the string
> > > > length (1 or 2 chars). Then the sorted key lookup itself should be
> > > > O(log n), which for a map of up to two elements is nothing.
> > > >
> > > > If using an unordered_map<> then hasing of the key (which [1] doesn't
> > > > report the complexity of) will take place, then the lookup should
> > > > happen in linear time.
> > > >
> > > > [1] https://en.cppreference.com/cpp/string/basic_string/hash
> > > > > finally the strings will be compared.
> > > > >
> > > >
> > > > When the LSC tables have to be updated because of a significant change
> > > > in colour temperature we interpolate vectors of thousands of elements
> > > > for the new colour temperature and then memcpy them to the parameters buffer.
> > > >
> > > > Before reworking anything here I would like to see a comparison of these
> > > > costs to the above described complexity of using a map of 4 elements
> > > > indexed by a string of 1 or 2 characters.
> > > >
> > > > And no, we're not:
> > > >
> > > > > > > going great lengths to
> > > > > > > write super optimized code striving for static allocations as much as
> > > > > > > possible
> > > >
> > > > Both when loading from yaml file and when re-sampling the polynomial
> > > > the destination vectors are dynamically allocatated. This is good, it
> > > > avoids relocations, but it's certainly not "static allocation as much
> > > > as possible" and I don't think anyone complained.
> > >
> > > I feel like I need to clarify my initial words. I'm well aware that a
> > > vector is dynamically allocating memory. The thing is that I felt bad
> > > for even having these vectors in the frame context because there is this
> > > "we'd eventually like to do everything static" mantra in my head. With
> > > the "great lengths" I thought more of the "super optimized" aspect. With
> > > static allocation I think more of things like the fixed number of entries
> > > in the FCQueue or the redesigned metadata with preallocated fixed memory
> > > buffers. I'm a big fan of keep it simple and let the language do it's
> > > thing. That is why camshark is full of maps and string lookups.
> > >
> > > Fixed FCQueue makes sense as it prevents reallocation during streaming.
> > > The Yaml parsing time
> > >
> >
> > Ok.
> >
> > How is any of this related to this patch and the string lookup though ?
> >
> > > >
> > > > > > > I was wondering if it is possible to template that using a static set as
> > > > > > > temüplate param and a fixed size array as container but then you can't
> > > > > > > configure that at init() time and end up with too much template
> > > > > > > magic....no
> > > > > > >
> > > > > > > As a compromise, would it make sense to index the map by an enum, so
> > > > > > > that we could define an enum for the possible types (it will most likely
> > > > > > > ever only be g,r,b,rg,bg (possible ir in the future). That would at
> > > > > > > least replace the string lookup by an int lookup, and we'd only have
> > > > > > > strings at yaml parse time?
> > > > >
> > > > > If we have an index, can't we just make it an array or vector ?
> > > > >
> > > >
> > > > Each IPA has different indexes that reflect the number of tables the
> > > > hadware supprots ('r' 'g' 'b' for mali, 'r' 'gr' 'gb' 'b' for rkisp1
> > > > etc). The current implementation allows the IPA to specify which keys
> > > > to use to parse the tables (or polynomials) from the tuning file and
> > > > then use the same keys for tables lookup when LSC have to be updated.
> > > >
> > > > This is of course the most flexible solution. We can provide a less
> > > > flexible interface to IPAs and assume up to 4 vectors are supported
> > > > and let IPA map the array entries to the colour component.
> > > >
> > > > Again, this seems largely unecessary to me considering the above
> > > > described cost of using a map is justified by a nicer API.
> > >
> > > I still think, that replacing the strings with an enum would keep the
> > > same flexibility, add a bit more compile time checking and maybe would
> > > even be a tad more efficient - but as you wrote that gain will be
> > > neglectable.
> > >
> >
> > Yes, neglectable, even more so considering what happens after the
> > lookup (interpolation and mem copies).
> >
> > I need to pass the string keys to parse the tuning file in at init()
> > time anyway and the IPA using the same identifiers for the lookup is
> > way nicer as an API instead of having to mix strings and enumerated
> > types.
>
> Why is that? You'd pass the list of enum entries in at init() time. This
> way the enum to string translation is all hidden inside libipa. The
> strings wouldn't be part of the API at all.
>
> >
> > For the compile time checking, I don't think this is a concern. We're
> > not talking of application provided input or application facing APIs,
> > we're talking about in-tree IPA code, it's not like you can write and
> > run an IPA and get a key identifier wrong. You'll notice quite soon I
> > guess (and in case of a wrong tuning file, you'll fail at init() time
> > anyway, so you'll never get to do a lookup).
>
> This is also the API that shall be used by others to implement their
> IPAs (not a super common usecase though). Agreed, we'll likely notice.
> But what is the use case of having free form strings there?

A nicer API. A simpler implementation that doesn't need to map enum
members to strings. No need to extend the libipa class if an IPA wants
to use 'x' 'y' and 'z' as keys.

Again the net gain of going for enum is almost zero, I would reverse
the question "what is the use case of having enum there?" because I
feel like there has been so far 0 valid arguments for changing this.

That said, I won't push anymore, if preference is for enum let's do
enum.

>
> Regards,
> Stefan
>
> >
> > Honestly, I would like to see more compelling arguments for changing
> > this implementation before doing it.
> >
> > > >
> > > > > > So is the only concern really comparing strings vs comparing integers
> > > > > > when looking up a key ? In the same code path that memcpys thousands
> > > > > > of values to program the lsc tables ?
> > > > > >
> > > > > > I feel like I'm missing something.
> > > > > >
> > > > > > > > +};
> > > > > > > > +
> > > > > > > > +template<typename T>
> > > > > > > > +class ComponentsMap : public std::map<unsigned int, Components<T>>
> > > > > > > > +{
> > > > > > > > +};
> > > > > > > > +
> > > > > > > > +} /* namespace lsc */
> > > > > > > > +
> > > > > > > > +struct LscDescriptor {
> > > > > > > > +       std::vector<std::string> keys;
> > > > > > > > +       unsigned int numHCells;
> > > > > > > > +       unsigned int numVCells;
> > > > > > > > +       Size sensorSize;
> > > > > > > > +};
> > > > > > > > +
> > > > > > > > +template<typename T, typename U>
> > > > > > > > +class LscImplementation
> > > > > > >
> > > > > > > On v1 Dan commented that U is not used in grid based. But it is used in
> > > > > > > the polynomial case. What about dropping T and defining it as
> > > > > > > U::QuantizedType? That would work for both cases and you get rid of one
> > > > > > > parameter. (And if there is a hardware that has a different register
> > > > > > > size than U::QuantizedType, the current solution would rely on luck,
> > > > > > > too.)
> > > > > >
> > > > > > This one is a good suggestion.
> > > > > >
> > > > > > Right now the template argument T serves only for loading values from
> > > > > > file in a type large enough to accommodate them
> > > > > >
> > > > > > Using U::QuantizedType seems like a good idea.
> > > > > > We're gonna drop it anyway an move to store and load float gains from
> > > > > > the tuning file, but in the interim using Q::QuantizedType seems good
> > > > > > enough.
> > > > > >
> > > > > > > > +{
> > > > > > > > +public:
> > > > > > > > +       virtual ~LscImplementation() = default;
> > > > > > > > +
> > > > > > > > +       virtual int parseLscData(const ValueNode &tuningData,
> > > > > > > > +                                const LscDescriptor &descriptor) = 0;
> > > > > > > > +
> > > > > > > > +       virtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> > > > > > > > +                                                     const std::vector<double> &xPos,
> > > > > > > > +                                                     const std::vector<double> &yPos) = 0;
> > > > > > > > +};
> > > > > > > > +
> > > > > > > > +} /* namespace ipa */
> > > > > > > > +
> > > > > > > > +} /* namespace libcamera */
> > > > >
> > > > > [snip]
> > > > >
> > > > > > > > diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
> > > > > > > > index f607d86c54c3..8d3f1883d6a5 100644
> > > > > > > > --- a/src/ipa/libipa/lsc_polynomial.cpp
> > > > > > > > +++ b/src/ipa/libipa/lsc_polynomial.cpp
> > > > > > > > @@ -1,12 +1,14 @@
> > > > > > > >  /* SPDX-License-Identifier: LGPL-2.1-or-later */
> > > > > > > >  /*
> > > > > > > > - * Copyright (C) 2024, Ideas On Board
> > > > > > > > + * Copyright (C) 2026, Ideas On Board
> > > > > > > >   *
> > > > > > > > - * Polynomial class to represent lens shading correction
> > > > > > > > + * Polynomial based lens shading correction
> > > > > > > >   */
> > > > > > > >
> > > > > > > >  #include "lsc_polynomial.h"
> > > > > > > >
> > > > > > > > +#include <assert.h>
> > > > > > > > +
> > > > > > > >  #include <libcamera/base/log.h>
> > > > > > > >
> > > > > > > >  /**
> > > > > > > > @@ -21,7 +23,7 @@ LOG_DEFINE_CATEGORY(LscPolynomial)
> > > > > > > >  namespace ipa {
> > > > > > > >
> > > > > > > >  /**
> > > > > > > > - * \class LscPolynomial
> > > > > > > > + * \class Polynomial
> > > > > > > >   * \brief Class for handling even polynomials used in lens shading correction
> > > > > > > >   *
> > > > > > > >   * Shading artifacts of camera lenses can be modeled using even radial
> > > > > > > > @@ -31,7 +33,7 @@ namespace ipa {
> > > > > > > >   */
> > > > > > > >
> > > > > > > >  /**
> > > > > > > > - * \fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > > > > > > > + * \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > > > > > > >                       double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> > > > > > > >                       double k4 = 0.0)
> > > > > > > >   * \brief Construct a polynomial using the given coefficients
> > > > > > > > @@ -45,7 +47,13 @@ namespace ipa {
> > > > > > > >   */
> > > > > > > >
> > > > > > > >  /**
> > > > > > > > - * \fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)
> > > > > > > > + * \fn Polynomial::Polynomial(const Polynomial &other)
> > > > > > > > + * \brief Construct a Polynomial by copy
> > > > > > > > + * \param[in] other The Polynomial to copy construct from
> > > > > > > > + */
> > > > > > > > +
> > > > > > > > +/**
> > > > > > > > + * \fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)
> > > > > > > >   * \brief Sample the polynomial at the given normalized pixel position
> > > > > > > >   *
> > > > > > > >   * This functions samples the polynomial at the given pixel position divided by
> > > > > > > > @@ -55,9 +63,21 @@ namespace ipa {
> > > > > > > >   * \param y y position in normalized coordinates
> > > > > > > >   * \return The sampled value
> > > > > > > >   */
> > > > > > > > +double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
> > > > > > > > +{
> > > > > > > > +       double dx = x - cnx_;
> > > > > > > > +       double dy = y - cny_;
> > > > > > > > +       double r = sqrt(dx * dx + dy * dy);
> > > > > > > > +       double res = 1.0;
> > > > > > > > +
> > > > > > > > +       for (unsigned int i = 0; i < coefficients_.size(); i++)
> > > > > > > > +               res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> > > > > > > > +
> > > > > > > > +       return res;
> > > > > > > > +}
> > > > > > >
> > > > > > > These are plain refactorings. I see that the whole libipa rework
> > > > > > > is difficult to do in small patches, but I think it would make review
> > > > > > > (and later history reading) easier if we'd cut out these refactorings
> > > > > > > into separate preparatory patches.
> > > > > >
> > > > > > I think we all knew that this work was going to be hard to review as
> > > > > > it was hard to implement.
> > > > > >
> > > > > > I again suggest to look at the final result more than the diff.
> > > > >
> > > > > :-(
> > > > >
> > > > > > I could break the patches down in small non-compilable units if it
> > > > > > helps.
> > >
> > > I'm not sure if non-compilable patches really help. But I think there
> > > are logical units that could be cut out (Like going from members in the
> > > components to a map) in a compilable way. I feel your pain and am at the
> > > same time a bit concerned that reading the history will be a pain if we
> > > do everything in one big step.
> > >
> >
> > I'll see what I can do then
> >
> > > Best regards,
> > > Stefan
> > >
> > > > >
> > > > > It will help review, if those smaller patches are self-contained and can
> > > > > be reviewed on their own, even if they don't necessarily compile on
> > > > > their own.
> > > > >
> > > > > > > >
> > > > > > > >  /**
> > > > > > > > - * \fn LscPolynomial::getM()
> > > > > > > > + * \fn Polynomial::getM()
> > > > > > > >   * \brief Get the value m as described in the dng specification
> > > > > > > >   *
> > > > > > > >   * Returns m according to dng spec. m represents the Euclidean distance
> > > > >
> > > > > [snip]
> > > > >
> > > > > > > > diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
> > > > > > > > index 0a256e225327..7c656d95acfb 100644
> > > > > > > > --- a/src/ipa/rkisp1/algorithms/lsc.h
> > > > > > > > +++ b/src/ipa/rkisp1/algorithms/lsc.h
> > > > > > > > @@ -2,17 +2,23 @@
> > > > > > > >  /*
> > > > > > > >   * Copyright (C) 2021-2022, Ideas On Board
> > > > > > > >   *
> > > > > > > > - * RkISP1 Lens Shading Correction control
> > > > > > > > + * RkISP1 Lens Shading Correction algorithm
> > > > > > > >   */
> > > > > > > >
> > > > > > > >  #pragma once
> > > > > > > >
> > > > > > > > -#include <map>
> > > > > > > > -#include <memory>
> > > > > > > > +#include <vector>
> > > > > > > >
> > > > > > > > -#include "libipa/interpolator.h"
> > > > > > > > +#include <linux/rkisp1-config.h>
> > > > > > > > +
> > > > > > > > +#include "libcamera/internal/value_node.h"
> > > > > > > > +
> > > > > > > > +#include "libipa/fixedpoint.h"
> > > > > > > > +#include "libipa/lsc.h"
> > > > > > > >
> > > > > > > >  #include "algorithm.h"
> > > > > > > > +#include "ipa_context.h"
> > > > > > > > +#include "params.h"
> > > > > > > >
> > > > > > > >  namespace libcamera {
> > > > > > > >
> > > > > > > > @@ -37,39 +43,28 @@ public:
> > > > > > > >                      const rkisp1_stat_buffer *stats,
> > > > > > > >                      ControlList &metadata) override;
> > > > > > > >
> > > > > > > > -       struct Components {
> > > > > > > > -               std::vector<uint16_t> r;
> > > > > > > > -               std::vector<uint16_t> gr;
> > > > > > > > -               std::vector<uint16_t> gb;
> > > > > > > > -               std::vector<uint16_t> b;
> > > > > > > > -       };
> > > > > > > > -
> > > > > > > > -       class ShadingDescriptor
> > > > > > > > -       {
> > > > > > > > -       public:
> > > > > > > > -               virtual ~ShadingDescriptor() = default;
> > > > > > > > -               virtual Components sampleForCrop(const Rectangle &cropRectangle,
> > > > > > > > -                                                Span<const double> xSizes,
> > > > > > > > -                                                Span<const double> ySizes) = 0;
> > > > > > > > -       };
> > > > > > > > -
> > > > > > > > -       using ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;
> > > > > > > > -
> > > > > > > >  private:
> > > > > > > > +       std::vector<double> parseSizes(const ValueNode &tuningData,
> > > > > > > > +                                      const char *prop);
> > > > > > > > +       std::vector<double> sizesToPositions(Span<const double> sizes);
> > > > > > > > +
> > > > > > > >         void setParameters(rkisp1_cif_isp_lsc_config &config);
> > > > > > > > -       void copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);
> > > > > > > > +       void copyTable(rkisp1_cif_isp_lsc_config &config,
> > > > > > > > +                      const ipa::lsc::Components<uint16_t> &set0);
> > > > > > > >
> > > > > > > > -       ShadingDescriptorMap shadingDescriptors_;
> > > > > > > > -       ipa::Interpolator<Components> sets_;
> > > > > > > >         std::vector<double> xSize_;
> > > > > > > >         std::vector<double> ySize_;
> > > > > > > >         uint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > > > >         uint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > > > >         uint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > > > >         uint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > > > > +       std::vector<double> xPos_;
> > > > > > > > +       std::vector<double> yPos_;
> > > > > > > >
> > > > > > > >         unsigned int lastAppliedCt_;
> > > > > > > >         unsigned int lastAppliedQuantizedCt_;
> > > > > > > > +
> > > > > > > > +       LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;
> > > > > > >
> > > > > > > I don't really like specifying the fixed format at compile time, as we
> > > > > > > will have a hard time to implement support for different formats at
> > > > > > > runtime like Mali supports. But I also don't see an easy way out of
> > > > > > > that.
> > > > > > >
> > > > > > > >  };
> > > > > > > >
> > > > > > > >  } /* namespace ipa::rkisp1::algorithms */
> > > > > > > > diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
> > > > > > > > index cd97e10bcf2b..005f4102b4f6 100644
> > > > > > > > --- a/src/ipa/rkisp1/ipa_context.h
> > > > > > > > +++ b/src/ipa/rkisp1/ipa_context.h
> > > > > > > > @@ -30,6 +30,7 @@
> > > > > > > >  #include "libipa/ccm.h"
> > > > > > > >  #include "libipa/fc_queue.h"
> > > > > > > >  #include "libipa/fixedpoint.h"
> > > > > > > > +#include "libipa/lsc.h"
> > > > > > > >
> > > > > > > >  namespace libcamera {
> > > > > > > >
> > > > > > > > @@ -138,9 +139,7 @@ struct IPAActiveState {
> > > > > > > >                 double strength;
> > > > > > > >         } wdr;
> > > > > > > >
> > > > > > > > -       struct {
> > > > > > > > -               bool enabled;
> > > > > > > > -       } lsc;
> > > > > > > > +       ipa::lsc::ActiveState lsc;
> > > > > > >
> > > > > > > That will really ease IPA development.
> > > > > > >
> > > > > > > >  };
> > > > > > > >
> > > > > > > >  struct IPAFrameContext : public FrameContext {
> > > > > > > > @@ -213,10 +212,7 @@ struct IPAFrameContext : public FrameContext {
> > > > > > > >                 double gain;
> > > > > > > >         } wdr;
> > > > > > > >
> > > > > > > > -       struct {
> > > > > > > > -               bool enabled;
> > > > > > > > -               bool update;
> > > > > > > > -       } lsc;
> > > > > > > > +       ipa::lsc::FrameContext lsc;
> > > > > > > >  };
> > > > > > >
> > > > > > > This is a really massive patch and I wonder if it would be possible to
> > > > > > > split it out into multiple changes (also in case we have regressions). I
> > > > > > > know this is more work, but otoh only rkisp1 uses that. So it would be
> > > > > > > possible?
> > > > > >
> > > > > > Everything is possible.
> > > > > >
> > > > > > I guess we'll miss the next release though.
> > > > >
> > > > > Don't worry about releases. They are just numbers. Let's move forward
> > > > > with this work, give it review priority, and merge it when it will be
> > > > > ready.
> > > > >
> > > > > > > >  struct IPAContext {
> > > > >
> > > > > --
> > > > > Regards,
> > > > >
> > > > > Laurent Pinchart
Stefan Klug June 29, 2026, 1:30 p.m. UTC | #14
Quoting Jacopo Mondi (2026-06-29 13:38:40)
> On Mon, Jun 29, 2026 at 01:24:45PM +0200, Stefan Klug wrote:
> > Quoting Jacopo Mondi (2026-06-29 11:09:55)
> > > On Mon, Jun 29, 2026 at 10:04:31AM +0200, Stefan Klug wrote:
> > > > Hi Jacopo,
> > > >
> > > > Quoting Jacopo Mondi (2026-06-27 11:24:21)
> > > > > Hi Laurent
> > > > >
> > > > > On Fri, Jun 26, 2026 at 10:51:51PM +0300, Laurent Pinchart wrote:
> > > > > > On Fri, Jun 26, 2026 at 04:26:53PM +0200, Jacopo Mondi wrote:
> > > > > > > On Fri, Jun 26, 2026 at 11:18:12AM +0200, Stefan Klug wrote:
> > > > > > > > Quoting Jacopo Mondi (2026-06-15 16:05:34)
> > > > > > > > > Introduce an Lsc algorithm implementation in libIPA.
> > > > > > > > >
> > > > > > > > > The LscAlgorithm class supports two algorithms: grid-based lsc and
> > > > > > > > > polynomial-based lsc.
> > > > > > > > >
> > > > > > > > > Both implementations have been generalized from the existing ones in the
> > > > > > > > > RkISP1 IPA.
> > > > > > > > >
> > > > > > > > > Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> > > > > > > > > ---
> > > > > > > > >  src/ipa/libipa/lsc.cpp            | 322 +++++++++++++++++++++++++++++
> > > > > > > > >  src/ipa/libipa/lsc.h              | 145 +++++++++++++
> > > > > > > > >  src/ipa/libipa/lsc_base.cpp       | 152 ++++++++++++++
> > > > > > > > >  src/ipa/libipa/lsc_base.h         |  59 ++++++
> > > > > > > > >  src/ipa/libipa/lsc_grid.cpp       |  33 +++
> > > > > > > > >  src/ipa/libipa/lsc_grid.h         | 113 ++++++++++
> > > > > > > > >  src/ipa/libipa/lsc_polynomial.cpp | 133 +++++++++++-
> > > > > > > > >  src/ipa/libipa/lsc_polynomial.h   | 135 ++++++++----
> > > > > > > > >  src/ipa/libipa/meson.build        |   5 +
> > > > > > > > >  src/ipa/rkisp1/algorithms/lsc.cpp | 424 +++++---------------------------------
> > > > > > > > >  src/ipa/rkisp1/algorithms/lsc.h   |  45 ++--
> > > > > > > > >  src/ipa/rkisp1/ipa_context.h      |  10 +-
> > > > > > > > >  12 files changed, 1126 insertions(+), 450 deletions(-)
> > > > > > > > >
> > > > > > > > > diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
> > > > > > > > > new file mode 100644
> > > > > > > > > index 000000000000..ae713a776778
> > > > > > > > > --- /dev/null
> > > > > > > > > +++ b/src/ipa/libipa/lsc.cpp
> > > > > > > > > @@ -0,0 +1,322 @@
> > > > > >
> > > > > > [snip]
> > > > > >
> > > > > > > > > +/**
> > > > > > > > > + * \class LscAlgorithm
> > > > > > > > > + * \brief libIPA lsc algorithm implementation
> > > > > > > > > + * \tparam T The type used to store the gain values when loaded from tuning file
> > > > > > > > > + * \tparam U The fixedpoint lsc engine register format
> > > > > > > > > + *
> > > > > > > > > + * Due to the optical characteristics of the lens, the light intensity received
> > > > > > > > > + * by the sensor is not uniform. The Lens Shading Correction algorithm applies
> > > > > > > > > + * multipliers to all pixels to compensate for the lens shading effect.
> > > > > > > > > + *
> > > > > > > > > + * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.
> > > > > > > > > + * It provides support for parsing the tuning file content and generate tables
> > > > > > > > > + * of per-colour temperature gains that IPA algorithms can use to program their
> > > > > > > > > + * lsc engine.
> > > > > > > > > + *
> > > > > > > > > + * The init() function parses the tuning file and loads the gain tables either
> > > > > > > > > + * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain
> > > > > > > > > + * tables are organized per-colour temperature with per-colour components gain
> > > > > > > > > + * vectors or polynomial coefficients. The colour components names are
> > > > > > > > > + * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc
> > > > > > > > > + * engine support 4 colour components (r, gr, gb, b), some only support 3 colour
> > > > > > > > > + * components (r, g, b). The name (and number) of the expected colour components
> > > > > > > > > + * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys
> > > > > > > > > + * field.
> > > > > > > > > + *
> > > > > > > > > + * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour
> > > > > > > > > + * components. The gain table has been here omitted, but the expected number
> > > > > > > >
> > > > > > > > Nit: s/here omitted/omitted here/
> > > > > > >
> > > > > > > I think 'here omitted' is correct
> > > > > >
> > > > > > "omitted here" sounds more correct.
> > > > > >
> > > > > > > > > + * of entries has be equal to
> > > > > > > >
> > > > > > > > Nit: has to be
> > > > > > > >
> > > > > > > > > + * LscDescriptor::numHCells * LscDescriptor::numVCells.
> > > > > > > > > + *
> > > > > > > > > + * \code{.yaml}
> > > > > > > > > + * - Lsc:
> > > > > > > > > + *    sets:
> > > > > > > > > + *      - ct: 2500
> > > > > > > > > + *        r: [
> > > > > > > > > + *             .. gains table omitted..
> > > > > > > > > + *        ]
> > > > > > > > > + *        g: [
> > > > > > > > > + *             .. gains table omitted..
> > > > > > > > > + *        ]
> > > > > > > > > + *        b: [
> > > > > > > > > + *             .. gains table omitted..
> > > > > > > > > + *        ]
> > > > > > > > > + *      - ct: 6500
> > > > > > > > > + *        r: [
> > > > > > > > > + *             .. gains table omitted..
> > > > > > > > > + *        ]
> > > > > > > > > + *        g: [
> > > > > > > > > + *             .. gains table omitted..
> > > > > > > > > + *        ]
> > > > > > > > > + *        b: [
> > > > > > > > > + *             .. gains table omitted..
> > > > > > > > > + *        ]
> > > > > > > > > + * \endcode
> > > > > > > > > + *
> > > > > > > > > + * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'
> > > > > > > > > + * colour components:
> > > > > > > > > + *
> > > > > > > > > + * \code{.yaml}
> > > > > > > > > + * - Lsc:
> > > > > > > > > + *    type: "polynomial"
> > > > > > > > > + *    sets:
> > > > > > > > > + *      - ct: 2500
> > > > > > > > > + *        r:
> > > > > > > > > + *          cx: 0.5006571711950275
> > > > > > > > > + *          cy: 0.510093737499277
> > > > > > > > > + *          k0: 1.5393282208428813
> > > > > > > > > + *          k1: -1.1434559757908016
> > > > > > > > > + *          k2: 4.332602305814554
> > > > > > > > > + *          k3: 0.0
> > > > > > > > > + *          k4: 0.0
> > > > > > > > > + *        gr:
> > > > > > > > > + *          cx: 0.5009320529087338
> > > > > > > > > + *          cy: 0.511208038949085
> > > > > > > > > + *          k0: 1.5634738574805407
> > > > > > > > > + *          k1: -1.5623484259968348
> > > > > > > > > + *          k2: 4.846686073656501
> > > > > > > > > + *          k3: 0.0
> > > > > > > > > + *          k4: 0.0
> > > > > > > > > + *        gb:
> > > > > > > > > + *          cx: 0.5012013290343839
> > > > > > > > > + *          cy: 0.5128251541578288
> > > > > > > > > + *          k0: 1.526147944919103
> > > > > > > > > + *          k1: -1.4316976083689723
> > > > > > > > > + *          k2: 4.792604063222728
> > > > > > > > > + *          k3: 0.0
> > > > > > > > > + *          k4: 0.0
> > > > > > > > > + *        b:
> > > > > > > > > + *          cx: 0.49864139511067784
> > > > > > > > > + *          cy: 0.5162095081739346
> > > > > > > > > + *          k0: 1.0405245474038738
> > > > > > > > > + *          k1: 0.05618339879447103
> > > > > > > > > + *          k2: 1.8792813594001752
> > > > > > > > > + *          k3: 0.0
> > > > > > > > > + *          k4: 0.0
> > > > > > > > > + *      - ct: 6000
> > > > > > > > > + *        r:
> > > > > > > > > + *          cx: 0.5006202239353942
> > > > > > > > > + *          cy: 0.5099531318307661
> > > > > > > > > + *          k0: 1.4702946023945032
> > > > > > > > > + *          k1: -0.8893767547927631
> > > > > > > > > + *          k2: 3.920547732201387
> > > > > > > > > + *          k3: 0.0
> > > > > > > > > + *          k4: 0.0
> > > > > > > > > + *        gr:
> > > > > > > > > + *          cx: 0.500907874178317
> > > > > > > > > + *          cy: 0.511084916024106
> > > > > > > > > + *          k0: 1.5336172760559457
> > > > > > > > > + *          k1: -1.39964026514435
> > > > > > > > > + *          k2: 4.565487728954618
> > > > > > > > > + *          k3: 0.0
> > > > > > > > > + *          k4: 0.0
> > > > > > > > > + *        gb:
> > > > > > > > > + *          cx: 0.5011898608900477
> > > > > > > > > + *          cy: 0.5126797906745105
> > > > > > > > > + *          k0: 1.5013145790354843
> > > > > > > > > + *          k1: -1.2747407173754124
> > > > > > > > > + *          k2: 4.514682876897286
> > > > > > > > > + *          k3: 0.0
> > > > > > > > > + *          k4: 0.0
> > > > > > > > > + *        b:
> > > > > > > > > + *          cx: 0.4987561413116136
> > > > > > > > > + *          cy: 0.5159619420778772
> > > > > > > > > + *          k0: 1.0102986422191802
> > > > > > > > > + *          k1: 0.13263449763985727
> > > > > > > > > + *          k2: 1.686556107316064
> > > > > > > > > + *          k3: 0.0
> > > > > > > > > + *          k4: 0.0
> > > > > > > > > + * \endcode
> > > > > > > > > + *
> > > > > > > > > + * The lsc tables or the polynomial definition are generated at tuning time
> > > > > > > > > + * using an image of known resolution which needs to be specified in
> > > > > > > > > + * LscDescriptor::sensorSize.
> > > > > > > > > + *
> > > > > > > > > + * At LscAlgorithm::configure() time the lsc tables are re-sampled on the
> > > > > > > > > + * sensor's crop rectangle in use to adapt them to the configuration in use for
> > > > > > > > > + * a streaming session. Polynomial lsc tables support re-sampling and can be
> > > > > > > > > + * applied to any sensor configuration. Grid-based lsc tables cannot be
> > > > > > > > > + * re-sampled and the configuration as parsed from the tuning file is used for
> > > > > > > > > + * all sensor configurations providing best-effort results.
> > > > > > > > > + *
> > > > > > > > > + * When the IPA algorithms wants to get access to the (resampled) tables to
> > > > > > > > > + * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get
> > > > > > > > > + * an lsc table interpolated by the LscAlgorithm class for the specified colour
> > > > > > > > > + * temperature. If the algorithm wants to access the non-interpolated tables it
> > > > > > > > > + * can retrieve them using LscAlgorithm::getComponents().
> > > > > > > > > + */
> > > > > >
> > > > > > [snip]
> > > > > >
> > > > > > > > > diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
> > > > > > > > > new file mode 100644
> > > > > > > > > index 000000000000..aecae305aa9c
> > > > > > > > > --- /dev/null
> > > > > > > > > +++ b/src/ipa/libipa/lsc_base.h
> > > > > > > > > @@ -0,0 +1,59 @@
> > > > > > > > > +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> > > > > > > > > +/*
> > > > > > > > > + * Copyright (C) 2026 Ideas on Board Oy
> > > > > > > > > + *
> > > > > > > > > + * Base classes and types for LSC algorithms implementations
> > > > > > > > > + */
> > > > > > > > > +
> > > > > > > > > +#pragma once
> > > > > > > > > +
> > > > > > > > > +#include <map>
> > > > > > > > > +#include <string>
> > > > > > > > > +#include <vector>
> > > > > > > > > +
> > > > > > > > > +#include <libcamera/geometry.h>
> > > > > > > > > +
> > > > > > > > > +#include "libcamera/internal/value_node.h"
> > > > > > > > > +
> > > > > > > > > +namespace libcamera {
> > > > > > > > > +
> > > > > > > > > +namespace ipa {
> > > > > > > > > +
> > > > > > > > > +namespace lsc {
> > > > > > > > > +
> > > > > > > > > +template<typename T>
> > > > > > > > > +class Components : public std::map<std::string, std::vector<T>>
> > > > > > > > > +{
> > > > > > > >
> > > > > > > > I'm a bit uneasy here. On the one hand we are going great lengths to
> > > > > > > > write super optimized code striving for static allocations as much as
> > > > > > > > possible and now we add a string based map lookup that is run per frame.
> > > > > > >
> > > > > > > How are 'string based lookup' and 'static memory allocation' related ?
> > > > > > >
> > > > > > > > On the other hand I don't actually know how expensive the lookup is.
> > > > > > >
> > > > > > > So, Component is used at init to parse the tuning file. Fine.
> > > > > > >
> > > > > > > At configure a set of Components is re-sampled (polynomial) or
> > > > > > > returned as parsed from config file. Fine.
> > > > > > >
> > > > > > > Now, the run-time.
> > > > > > >
> > > > > > > I presume you're thinking at (using rkisp1 as an example)
> > > > > > >
> > > > > > >  void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
> > > > > > > -                                     const Components &set)
> > > > > > > +                                     const ipa::lsc::Components<uint16_t> &set)
> > > > > > >  {
> > > > > > > -       std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
> > > > > > > -       std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
> > > > > > > -       std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
> > > > > > > -       std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
> > > > > > > +       const auto &r = set.at("r");
> > > > > > > +       std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
> > > > > > > +       const auto &gr = set.at("gr");
> > > > > > > +       std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
> > > > > > > +       const auto &gb = set.at("gb");
> > > > > > > +       std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
> > > > > > > +       const auto &b = set.at("b");
> > > > > > > +       std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
> > > > > > >  }
> > > > > > >
> > > > > > > yes, we do a lookup on a string.
> > > > > > >
> > > > > > > The existing implementation stores the Components vectors in a struct
> > > > > > >
> > > > > > >     struct Components {
> > > > > > >             uint32_t ct;
> > > > > > >             std::vector<uint16_t> r;
> > > > > > >             std::vector<uint16_t> gr;
> > > > > > >             std::vector<uint16_t> gb;
> > > > > > >             std::vector<uint16_t> b;
> > > > > > >     };
> > > > > > >
> > > > > > > This patch instead stores them in a map.
> > > > > > >
> > > > > > > Now, not an STL guru here, but I presume that if your only concern is
> > > > > > > the lookup, a map with 4 elements top shouldn't be that hard.
> > > > > > >
> > > > > > > If there are implications I'm missing about storing the vectors in the
> > > > > > > map instead of storing them in a struct I would be glad to learn about
> > > > > > > that.
> > > > > >
> > > > > > A map with string keys will be significantly less efficient. First the
> > > > > > string will be hashed, then the hash will be used in the lookup, and
> > > > >
> > > > > Are you sure hashing happen for std::map too or is it just for
> > > > > std::unordered_map ?
> > > > >
> > > > > As far as I know, std::map sorts the keys by comparing them and
> > > > > the default Compare function is std::less<> which for std::string calls
> > > > > std::string::operator< which has a complexity linear in the string
> > > > > length (1 or 2 chars). Then the sorted key lookup itself should be
> > > > > O(log n), which for a map of up to two elements is nothing.
> > > > >
> > > > > If using an unordered_map<> then hasing of the key (which [1] doesn't
> > > > > report the complexity of) will take place, then the lookup should
> > > > > happen in linear time.
> > > > >
> > > > > [1] https://en.cppreference.com/cpp/string/basic_string/hash
> > > > > > finally the strings will be compared.
> > > > > >
> > > > >
> > > > > When the LSC tables have to be updated because of a significant change
> > > > > in colour temperature we interpolate vectors of thousands of elements
> > > > > for the new colour temperature and then memcpy them to the parameters buffer.
> > > > >
> > > > > Before reworking anything here I would like to see a comparison of these
> > > > > costs to the above described complexity of using a map of 4 elements
> > > > > indexed by a string of 1 or 2 characters.
> > > > >
> > > > > And no, we're not:
> > > > >
> > > > > > > > going great lengths to
> > > > > > > > write super optimized code striving for static allocations as much as
> > > > > > > > possible
> > > > >
> > > > > Both when loading from yaml file and when re-sampling the polynomial
> > > > > the destination vectors are dynamically allocatated. This is good, it
> > > > > avoids relocations, but it's certainly not "static allocation as much
> > > > > as possible" and I don't think anyone complained.
> > > >
> > > > I feel like I need to clarify my initial words. I'm well aware that a
> > > > vector is dynamically allocating memory. The thing is that I felt bad
> > > > for even having these vectors in the frame context because there is this
> > > > "we'd eventually like to do everything static" mantra in my head. With
> > > > the "great lengths" I thought more of the "super optimized" aspect. With
> > > > static allocation I think more of things like the fixed number of entries
> > > > in the FCQueue or the redesigned metadata with preallocated fixed memory
> > > > buffers. I'm a big fan of keep it simple and let the language do it's
> > > > thing. That is why camshark is full of maps and string lookups.
> > > >
> > > > Fixed FCQueue makes sense as it prevents reallocation during streaming.
> > > > The Yaml parsing time
> > > >
> > >
> > > Ok.
> > >
> > > How is any of this related to this patch and the string lookup though ?
> > >
> > > > >
> > > > > > > > I was wondering if it is possible to template that using a static set as
> > > > > > > > temüplate param and a fixed size array as container but then you can't
> > > > > > > > configure that at init() time and end up with too much template
> > > > > > > > magic....no
> > > > > > > >
> > > > > > > > As a compromise, would it make sense to index the map by an enum, so
> > > > > > > > that we could define an enum for the possible types (it will most likely
> > > > > > > > ever only be g,r,b,rg,bg (possible ir in the future). That would at
> > > > > > > > least replace the string lookup by an int lookup, and we'd only have
> > > > > > > > strings at yaml parse time?
> > > > > >
> > > > > > If we have an index, can't we just make it an array or vector ?
> > > > > >
> > > > >
> > > > > Each IPA has different indexes that reflect the number of tables the
> > > > > hadware supprots ('r' 'g' 'b' for mali, 'r' 'gr' 'gb' 'b' for rkisp1
> > > > > etc). The current implementation allows the IPA to specify which keys
> > > > > to use to parse the tables (or polynomials) from the tuning file and
> > > > > then use the same keys for tables lookup when LSC have to be updated.
> > > > >
> > > > > This is of course the most flexible solution. We can provide a less
> > > > > flexible interface to IPAs and assume up to 4 vectors are supported
> > > > > and let IPA map the array entries to the colour component.
> > > > >
> > > > > Again, this seems largely unecessary to me considering the above
> > > > > described cost of using a map is justified by a nicer API.
> > > >
> > > > I still think, that replacing the strings with an enum would keep the
> > > > same flexibility, add a bit more compile time checking and maybe would
> > > > even be a tad more efficient - but as you wrote that gain will be
> > > > neglectable.
> > > >
> > >
> > > Yes, neglectable, even more so considering what happens after the
> > > lookup (interpolation and mem copies).
> > >
> > > I need to pass the string keys to parse the tuning file in at init()
> > > time anyway and the IPA using the same identifiers for the lookup is
> > > way nicer as an API instead of having to mix strings and enumerated
> > > types.
> >
> > Why is that? You'd pass the list of enum entries in at init() time. This
> > way the enum to string translation is all hidden inside libipa. The
> > strings wouldn't be part of the API at all.
> >
> > >
> > > For the compile time checking, I don't think this is a concern. We're
> > > not talking of application provided input or application facing APIs,
> > > we're talking about in-tree IPA code, it's not like you can write and
> > > run an IPA and get a key identifier wrong. You'll notice quite soon I
> > > guess (and in case of a wrong tuning file, you'll fail at init() time
> > > anyway, so you'll never get to do a lookup).
> >
> > This is also the API that shall be used by others to implement their
> > IPAs (not a super common usecase though). Agreed, we'll likely notice.
> > But what is the use case of having free form strings there?
> 
> A nicer API. A simpler implementation that doesn't need to map enum
> members to strings. No need to extend the libipa class if an IPA wants
> to use 'x' 'y' and 'z' as keys.
> 
> Again the net gain of going for enum is almost zero, I would reverse
> the question "what is the use case of having enum there?" because I
> feel like there has been so far 0 valid arguments for changing this.
> 
> That said, I won't push anymore, if preference is for enum let's do
> enum.

Me neither. In the last message I only wanted to clarify that I didn't
envision a API with mixed strings/enums. Maybe others also have
opinions. I won't block on this one.

Cheers,
Stefan

> 
> >
> > Regards,
> > Stefan
> >
> > >
> > > Honestly, I would like to see more compelling arguments for changing
> > > this implementation before doing it.
> > >
> > > > >
> > > > > > > So is the only concern really comparing strings vs comparing integers
> > > > > > > when looking up a key ? In the same code path that memcpys thousands
> > > > > > > of values to program the lsc tables ?
> > > > > > >
> > > > > > > I feel like I'm missing something.
> > > > > > >
> > > > > > > > > +};
> > > > > > > > > +
> > > > > > > > > +template<typename T>
> > > > > > > > > +class ComponentsMap : public std::map<unsigned int, Components<T>>
> > > > > > > > > +{
> > > > > > > > > +};
> > > > > > > > > +
> > > > > > > > > +} /* namespace lsc */
> > > > > > > > > +
> > > > > > > > > +struct LscDescriptor {
> > > > > > > > > +       std::vector<std::string> keys;
> > > > > > > > > +       unsigned int numHCells;
> > > > > > > > > +       unsigned int numVCells;
> > > > > > > > > +       Size sensorSize;
> > > > > > > > > +};
> > > > > > > > > +
> > > > > > > > > +template<typename T, typename U>
> > > > > > > > > +class LscImplementation
> > > > > > > >
> > > > > > > > On v1 Dan commented that U is not used in grid based. But it is used in
> > > > > > > > the polynomial case. What about dropping T and defining it as
> > > > > > > > U::QuantizedType? That would work for both cases and you get rid of one
> > > > > > > > parameter. (And if there is a hardware that has a different register
> > > > > > > > size than U::QuantizedType, the current solution would rely on luck,
> > > > > > > > too.)
> > > > > > >
> > > > > > > This one is a good suggestion.
> > > > > > >
> > > > > > > Right now the template argument T serves only for loading values from
> > > > > > > file in a type large enough to accommodate them
> > > > > > >
> > > > > > > Using U::QuantizedType seems like a good idea.
> > > > > > > We're gonna drop it anyway an move to store and load float gains from
> > > > > > > the tuning file, but in the interim using Q::QuantizedType seems good
> > > > > > > enough.
> > > > > > >
> > > > > > > > > +{
> > > > > > > > > +public:
> > > > > > > > > +       virtual ~LscImplementation() = default;
> > > > > > > > > +
> > > > > > > > > +       virtual int parseLscData(const ValueNode &tuningData,
> > > > > > > > > +                                const LscDescriptor &descriptor) = 0;
> > > > > > > > > +
> > > > > > > > > +       virtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
> > > > > > > > > +                                                     const std::vector<double> &xPos,
> > > > > > > > > +                                                     const std::vector<double> &yPos) = 0;
> > > > > > > > > +};
> > > > > > > > > +
> > > > > > > > > +} /* namespace ipa */
> > > > > > > > > +
> > > > > > > > > +} /* namespace libcamera */
> > > > > >
> > > > > > [snip]
> > > > > >
> > > > > > > > > diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
> > > > > > > > > index f607d86c54c3..8d3f1883d6a5 100644
> > > > > > > > > --- a/src/ipa/libipa/lsc_polynomial.cpp
> > > > > > > > > +++ b/src/ipa/libipa/lsc_polynomial.cpp
> > > > > > > > > @@ -1,12 +1,14 @@
> > > > > > > > >  /* SPDX-License-Identifier: LGPL-2.1-or-later */
> > > > > > > > >  /*
> > > > > > > > > - * Copyright (C) 2024, Ideas On Board
> > > > > > > > > + * Copyright (C) 2026, Ideas On Board
> > > > > > > > >   *
> > > > > > > > > - * Polynomial class to represent lens shading correction
> > > > > > > > > + * Polynomial based lens shading correction
> > > > > > > > >   */
> > > > > > > > >
> > > > > > > > >  #include "lsc_polynomial.h"
> > > > > > > > >
> > > > > > > > > +#include <assert.h>
> > > > > > > > > +
> > > > > > > > >  #include <libcamera/base/log.h>
> > > > > > > > >
> > > > > > > > >  /**
> > > > > > > > > @@ -21,7 +23,7 @@ LOG_DEFINE_CATEGORY(LscPolynomial)
> > > > > > > > >  namespace ipa {
> > > > > > > > >
> > > > > > > > >  /**
> > > > > > > > > - * \class LscPolynomial
> > > > > > > > > + * \class Polynomial
> > > > > > > > >   * \brief Class for handling even polynomials used in lens shading correction
> > > > > > > > >   *
> > > > > > > > >   * Shading artifacts of camera lenses can be modeled using even radial
> > > > > > > > > @@ -31,7 +33,7 @@ namespace ipa {
> > > > > > > > >   */
> > > > > > > > >
> > > > > > > > >  /**
> > > > > > > > > - * \fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > > > > > > > > + * \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
> > > > > > > > >                       double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
> > > > > > > > >                       double k4 = 0.0)
> > > > > > > > >   * \brief Construct a polynomial using the given coefficients
> > > > > > > > > @@ -45,7 +47,13 @@ namespace ipa {
> > > > > > > > >   */
> > > > > > > > >
> > > > > > > > >  /**
> > > > > > > > > - * \fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)
> > > > > > > > > + * \fn Polynomial::Polynomial(const Polynomial &other)
> > > > > > > > > + * \brief Construct a Polynomial by copy
> > > > > > > > > + * \param[in] other The Polynomial to copy construct from
> > > > > > > > > + */
> > > > > > > > > +
> > > > > > > > > +/**
> > > > > > > > > + * \fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)
> > > > > > > > >   * \brief Sample the polynomial at the given normalized pixel position
> > > > > > > > >   *
> > > > > > > > >   * This functions samples the polynomial at the given pixel position divided by
> > > > > > > > > @@ -55,9 +63,21 @@ namespace ipa {
> > > > > > > > >   * \param y y position in normalized coordinates
> > > > > > > > >   * \return The sampled value
> > > > > > > > >   */
> > > > > > > > > +double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
> > > > > > > > > +{
> > > > > > > > > +       double dx = x - cnx_;
> > > > > > > > > +       double dy = y - cny_;
> > > > > > > > > +       double r = sqrt(dx * dx + dy * dy);
> > > > > > > > > +       double res = 1.0;
> > > > > > > > > +
> > > > > > > > > +       for (unsigned int i = 0; i < coefficients_.size(); i++)
> > > > > > > > > +               res += coefficients_[i] * std::pow(r, (i + 1) * 2);
> > > > > > > > > +
> > > > > > > > > +       return res;
> > > > > > > > > +}
> > > > > > > >
> > > > > > > > These are plain refactorings. I see that the whole libipa rework
> > > > > > > > is difficult to do in small patches, but I think it would make review
> > > > > > > > (and later history reading) easier if we'd cut out these refactorings
> > > > > > > > into separate preparatory patches.
> > > > > > >
> > > > > > > I think we all knew that this work was going to be hard to review as
> > > > > > > it was hard to implement.
> > > > > > >
> > > > > > > I again suggest to look at the final result more than the diff.
> > > > > >
> > > > > > :-(
> > > > > >
> > > > > > > I could break the patches down in small non-compilable units if it
> > > > > > > helps.
> > > >
> > > > I'm not sure if non-compilable patches really help. But I think there
> > > > are logical units that could be cut out (Like going from members in the
> > > > components to a map) in a compilable way. I feel your pain and am at the
> > > > same time a bit concerned that reading the history will be a pain if we
> > > > do everything in one big step.
> > > >
> > >
> > > I'll see what I can do then
> > >
> > > > Best regards,
> > > > Stefan
> > > >
> > > > > >
> > > > > > It will help review, if those smaller patches are self-contained and can
> > > > > > be reviewed on their own, even if they don't necessarily compile on
> > > > > > their own.
> > > > > >
> > > > > > > > >
> > > > > > > > >  /**
> > > > > > > > > - * \fn LscPolynomial::getM()
> > > > > > > > > + * \fn Polynomial::getM()
> > > > > > > > >   * \brief Get the value m as described in the dng specification
> > > > > > > > >   *
> > > > > > > > >   * Returns m according to dng spec. m represents the Euclidean distance
> > > > > >
> > > > > > [snip]
> > > > > >
> > > > > > > > > diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
> > > > > > > > > index 0a256e225327..7c656d95acfb 100644
> > > > > > > > > --- a/src/ipa/rkisp1/algorithms/lsc.h
> > > > > > > > > +++ b/src/ipa/rkisp1/algorithms/lsc.h
> > > > > > > > > @@ -2,17 +2,23 @@
> > > > > > > > >  /*
> > > > > > > > >   * Copyright (C) 2021-2022, Ideas On Board
> > > > > > > > >   *
> > > > > > > > > - * RkISP1 Lens Shading Correction control
> > > > > > > > > + * RkISP1 Lens Shading Correction algorithm
> > > > > > > > >   */
> > > > > > > > >
> > > > > > > > >  #pragma once
> > > > > > > > >
> > > > > > > > > -#include <map>
> > > > > > > > > -#include <memory>
> > > > > > > > > +#include <vector>
> > > > > > > > >
> > > > > > > > > -#include "libipa/interpolator.h"
> > > > > > > > > +#include <linux/rkisp1-config.h>
> > > > > > > > > +
> > > > > > > > > +#include "libcamera/internal/value_node.h"
> > > > > > > > > +
> > > > > > > > > +#include "libipa/fixedpoint.h"
> > > > > > > > > +#include "libipa/lsc.h"
> > > > > > > > >
> > > > > > > > >  #include "algorithm.h"
> > > > > > > > > +#include "ipa_context.h"
> > > > > > > > > +#include "params.h"
> > > > > > > > >
> > > > > > > > >  namespace libcamera {
> > > > > > > > >
> > > > > > > > > @@ -37,39 +43,28 @@ public:
> > > > > > > > >                      const rkisp1_stat_buffer *stats,
> > > > > > > > >                      ControlList &metadata) override;
> > > > > > > > >
> > > > > > > > > -       struct Components {
> > > > > > > > > -               std::vector<uint16_t> r;
> > > > > > > > > -               std::vector<uint16_t> gr;
> > > > > > > > > -               std::vector<uint16_t> gb;
> > > > > > > > > -               std::vector<uint16_t> b;
> > > > > > > > > -       };
> > > > > > > > > -
> > > > > > > > > -       class ShadingDescriptor
> > > > > > > > > -       {
> > > > > > > > > -       public:
> > > > > > > > > -               virtual ~ShadingDescriptor() = default;
> > > > > > > > > -               virtual Components sampleForCrop(const Rectangle &cropRectangle,
> > > > > > > > > -                                                Span<const double> xSizes,
> > > > > > > > > -                                                Span<const double> ySizes) = 0;
> > > > > > > > > -       };
> > > > > > > > > -
> > > > > > > > > -       using ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;
> > > > > > > > > -
> > > > > > > > >  private:
> > > > > > > > > +       std::vector<double> parseSizes(const ValueNode &tuningData,
> > > > > > > > > +                                      const char *prop);
> > > > > > > > > +       std::vector<double> sizesToPositions(Span<const double> sizes);
> > > > > > > > > +
> > > > > > > > >         void setParameters(rkisp1_cif_isp_lsc_config &config);
> > > > > > > > > -       void copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);
> > > > > > > > > +       void copyTable(rkisp1_cif_isp_lsc_config &config,
> > > > > > > > > +                      const ipa::lsc::Components<uint16_t> &set0);
> > > > > > > > >
> > > > > > > > > -       ShadingDescriptorMap shadingDescriptors_;
> > > > > > > > > -       ipa::Interpolator<Components> sets_;
> > > > > > > > >         std::vector<double> xSize_;
> > > > > > > > >         std::vector<double> ySize_;
> > > > > > > > >         uint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > > > > >         uint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > > > > >         uint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > > > > >         uint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
> > > > > > > > > +       std::vector<double> xPos_;
> > > > > > > > > +       std::vector<double> yPos_;
> > > > > > > > >
> > > > > > > > >         unsigned int lastAppliedCt_;
> > > > > > > > >         unsigned int lastAppliedQuantizedCt_;
> > > > > > > > > +
> > > > > > > > > +       LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;
> > > > > > > >
> > > > > > > > I don't really like specifying the fixed format at compile time, as we
> > > > > > > > will have a hard time to implement support for different formats at
> > > > > > > > runtime like Mali supports. But I also don't see an easy way out of
> > > > > > > > that.
> > > > > > > >
> > > > > > > > >  };
> > > > > > > > >
> > > > > > > > >  } /* namespace ipa::rkisp1::algorithms */
> > > > > > > > > diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
> > > > > > > > > index cd97e10bcf2b..005f4102b4f6 100644
> > > > > > > > > --- a/src/ipa/rkisp1/ipa_context.h
> > > > > > > > > +++ b/src/ipa/rkisp1/ipa_context.h
> > > > > > > > > @@ -30,6 +30,7 @@
> > > > > > > > >  #include "libipa/ccm.h"
> > > > > > > > >  #include "libipa/fc_queue.h"
> > > > > > > > >  #include "libipa/fixedpoint.h"
> > > > > > > > > +#include "libipa/lsc.h"
> > > > > > > > >
> > > > > > > > >  namespace libcamera {
> > > > > > > > >
> > > > > > > > > @@ -138,9 +139,7 @@ struct IPAActiveState {
> > > > > > > > >                 double strength;
> > > > > > > > >         } wdr;
> > > > > > > > >
> > > > > > > > > -       struct {
> > > > > > > > > -               bool enabled;
> > > > > > > > > -       } lsc;
> > > > > > > > > +       ipa::lsc::ActiveState lsc;
> > > > > > > >
> > > > > > > > That will really ease IPA development.
> > > > > > > >
> > > > > > > > >  };
> > > > > > > > >
> > > > > > > > >  struct IPAFrameContext : public FrameContext {
> > > > > > > > > @@ -213,10 +212,7 @@ struct IPAFrameContext : public FrameContext {
> > > > > > > > >                 double gain;
> > > > > > > > >         } wdr;
> > > > > > > > >
> > > > > > > > > -       struct {
> > > > > > > > > -               bool enabled;
> > > > > > > > > -               bool update;
> > > > > > > > > -       } lsc;
> > > > > > > > > +       ipa::lsc::FrameContext lsc;
> > > > > > > > >  };
> > > > > > > >
> > > > > > > > This is a really massive patch and I wonder if it would be possible to
> > > > > > > > split it out into multiple changes (also in case we have regressions). I
> > > > > > > > know this is more work, but otoh only rkisp1 uses that. So it would be
> > > > > > > > possible?
> > > > > > >
> > > > > > > Everything is possible.
> > > > > > >
> > > > > > > I guess we'll miss the next release though.
> > > > > >
> > > > > > Don't worry about releases. They are just numbers. Let's move forward
> > > > > > with this work, give it review priority, and merge it when it will be
> > > > > > ready.
> > > > > >
> > > > > > > > >  struct IPAContext {
> > > > > >
> > > > > > --
> > > > > > Regards,
> > > > > >
> > > > > > Laurent Pinchart

Patch
diff mbox series

diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
new file mode 100644
index 000000000000..ae713a776778
--- /dev/null
+++ b/src/ipa/libipa/lsc.cpp
@@ -0,0 +1,322 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Ideas on Board Oy
+ *
+ * libIPA Lsc algorithms
+ */
+
+#include "lsc.h"
+
+#include <libcamera/base/log.h>
+
+/**
+ * \file lsc.h
+ * \brief libipa lsc algorithm
+ */
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(Lsc)
+
+namespace ipa {
+
+namespace lsc {
+
+/**
+ * \struct ActiveState
+ * \brief The lsc active state
+ *
+ * \var ActiveState::enabled
+ * \brief Boolean flag for the LscAlgorithm enable status
+ */
+
+/**
+ * \struct FrameContext
+ * \brief The lsc frame context
+ *
+ * \var FrameContext::enabled
+ * \brief Boolean flag for the LscAlgorithm enable status
+ *
+ * \var FrameContext::update
+ * \brief Boolean flag for the LscAlgorithm updated status
+ */
+
+} /* namespace lsc */
+
+#ifndef __DOXYGEN__
+template<>
+void Interpolator<lsc::Components<uint16_t>>::
+	interpolate(const lsc::Components<uint16_t> &a,
+		    const lsc::Components<uint16_t> &b,
+		    lsc::Components<uint16_t> &dest,
+		    double lambda)
+{
+	for (auto const &[k, v] : a)
+		interpolateVector(v, b.at(k), dest[k], lambda);
+}
+#endif
+
+/**
+ * \class LscAlgorithmBase
+ * \brief Base class for LscAlgorithm
+ *
+ * Base class for LscAlgorithm for non-templated functions implementation
+ */
+
+/**
+ * \brief Queue a request to the lsc algorithm
+ * \param[in] state The lsc active state
+ * \param[in] context The lsc frame context
+ * \param[in] controls The list of controls associated with a Request
+ *
+ * Queue a new list of \a controls to the lsc algorithm.
+ * The only supported control is controls::LensShadingCorrectionEnable.
+ */
+void LscAlgorithmBase::queueRequest(lsc::ActiveState &state,
+				    lsc::FrameContext &context,
+				    const ControlList &controls)
+{
+	const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);
+	if (lscEnable && *lscEnable != state.enabled) {
+		state.enabled = *lscEnable;
+
+		LOG(Lsc, Debug)
+			<< (state.enabled ? "Enabling" : "Disabling") << " Lsc";
+
+		context.update = true;
+	}
+
+	context.enabled = state.enabled;
+}
+
+/**
+ * \brief Populate the list of lsc metadata
+ * \param[in] context The lsc frame context
+ * \param[in] metadata The list of metadata
+ *
+ * Populates the list of \a metadata with controls handled by the LscAlgorithm
+ * class. The only supported metadata is controls::LensShadingCorrectionEnable.
+ */
+void LscAlgorithmBase::process(lsc::FrameContext &context, ControlList &metadata)
+{
+	metadata.set(controls::LensShadingCorrectionEnable, context.enabled);
+}
+
+/**
+ * \class LscAlgorithm
+ * \brief libIPA lsc algorithm implementation
+ * \tparam T The type used to store the gain values when loaded from tuning file
+ * \tparam U The fixedpoint lsc engine register format
+ *
+ * Due to the optical characteristics of the lens, the light intensity received
+ * by the sensor is not uniform. The Lens Shading Correction algorithm applies
+ * multipliers to all pixels to compensate for the lens shading effect.
+ *
+ * The LscAlgorithm implements the libipa Lens Shading Correction algorithm.
+ * It provides support for parsing the tuning file content and generate tables
+ * of per-colour temperature gains that IPA algorithms can use to program their
+ * lsc engine.
+ *
+ * The init() function parses the tuning file and loads the gain tables either
+ * in tabular form (LscGrid) or as radial polynomial (LscPolynomial). The gain
+ * tables are organized per-colour temperature with per-colour components gain
+ * vectors or polynomial coefficients. The colour components names are
+ * IPA-implementation specific and depend on the ISP lsc engine design. Some lsc
+ * engine support 4 colour components (r, gr, gb, b), some only support 3 colour
+ * components (r, g, b). The name (and number) of the expected colour components
+ * shall be provided to LscAlgorithm::init() using the LscDescriptor::keys
+ * field.
+ *
+ * Example of a tabular lens shading tuning file with 'r', 'g' and 'b' colour
+ * components. The gain table has been here omitted, but the expected number
+ * of entries has be equal to
+ * LscDescriptor::numHCells * LscDescriptor::numVCells.
+ *
+ * \code{.yaml}
+ * - Lsc:
+ *    sets:
+ *      - ct: 2500
+ *        r: [
+ *        	.. gains table omitted..
+ *        ]
+ *        g: [
+ *        	.. gains table omitted..
+ *        ]
+ *        b: [
+ *        	.. gains table omitted..
+ *        ]
+ *      - ct: 6500
+ *        r: [
+ *        	.. gains table omitted..
+ *        ]
+ *        g: [
+ *        	.. gains table omitted..
+ *        ]
+ *        b: [
+ *        	.. gains table omitted..
+ *        ]
+ * \endcode
+ *
+ * Example of a polynomial lens shading tuning file with 'r', 'gr', 'gb' and 'b'
+ * colour components:
+ *
+ * \code{.yaml}
+ * - Lsc:
+ *    type: "polynomial"
+ *    sets:
+ *      - ct: 2500
+ *        r:
+ *          cx: 0.5006571711950275
+ *          cy: 0.510093737499277
+ *          k0: 1.5393282208428813
+ *          k1: -1.1434559757908016
+ *          k2: 4.332602305814554
+ *          k3: 0.0
+ *          k4: 0.0
+ *        gr:
+ *          cx: 0.5009320529087338
+ *          cy: 0.511208038949085
+ *          k0: 1.5634738574805407
+ *          k1: -1.5623484259968348
+ *          k2: 4.846686073656501
+ *          k3: 0.0
+ *          k4: 0.0
+ *        gb:
+ *          cx: 0.5012013290343839
+ *          cy: 0.5128251541578288
+ *          k0: 1.526147944919103
+ *          k1: -1.4316976083689723
+ *          k2: 4.792604063222728
+ *          k3: 0.0
+ *          k4: 0.0
+ *        b:
+ *          cx: 0.49864139511067784
+ *          cy: 0.5162095081739346
+ *          k0: 1.0405245474038738
+ *          k1: 0.05618339879447103
+ *          k2: 1.8792813594001752
+ *          k3: 0.0
+ *          k4: 0.0
+ *      - ct: 6000
+ *        r:
+ *          cx: 0.5006202239353942
+ *          cy: 0.5099531318307661
+ *          k0: 1.4702946023945032
+ *          k1: -0.8893767547927631
+ *          k2: 3.920547732201387
+ *          k3: 0.0
+ *          k4: 0.0
+ *        gr:
+ *          cx: 0.500907874178317
+ *          cy: 0.511084916024106
+ *          k0: 1.5336172760559457
+ *          k1: -1.39964026514435
+ *          k2: 4.565487728954618
+ *          k3: 0.0
+ *          k4: 0.0
+ *        gb:
+ *          cx: 0.5011898608900477
+ *          cy: 0.5126797906745105
+ *          k0: 1.5013145790354843
+ *          k1: -1.2747407173754124
+ *          k2: 4.514682876897286
+ *          k3: 0.0
+ *          k4: 0.0
+ *        b:
+ *          cx: 0.4987561413116136
+ *          cy: 0.5159619420778772
+ *          k0: 1.0102986422191802
+ *          k1: 0.13263449763985727
+ *          k2: 1.686556107316064
+ *          k3: 0.0
+ *          k4: 0.0
+ * \endcode
+ *
+ * The lsc tables or the polynomial definition are generated at tuning time
+ * using an image of known resolution which needs to be specified in
+ * LscDescriptor::sensorSize.
+ *
+ * At LscAlgorithm::configure() time the lsc tables are re-sampled on the
+ * sensor's crop rectangle in use to adapt them to the configuration in use for
+ * a streaming session. Polynomial lsc tables support re-sampling and can be
+ * applied to any sensor configuration. Grid-based lsc tables cannot be
+ * re-sampled and the configuration as parsed from the tuning file is used for
+ * all sensor configurations providing best-effort results.
+ *
+ * When the IPA algorithms wants to get access to the (resampled) tables to
+ * program its lsc engine, it uses LscAlgorithm::interpolateComponents() to get
+ * an lsc table interpolated by the LscAlgorithm class for the specified colour
+ * temperature. If the algorithm wants to access the non-interpolated tables it
+ * can retrieve them using LscAlgorithm::getComponents().
+ */
+
+/**
+ * \fn LscAlgorithm::init()
+ * \param[in] tuningData The tuning data
+ * \param[in] controls The IPA list of supported controls
+ * \param[in] descriptor The lsc engine descriptor
+ *
+ * Parse \a tuningData according to the settings specified in \a descriptor to
+ * populate the lsc data and registers lsc controls in \a controls.
+ *
+ * \return 0 on success, a negative error code otherwise
+ */
+
+/**
+ * \fn LscAlgorithm::configure()
+ * \param[in] state The lsc active state
+ * \param[in] analogCrop The current sensor analog crop rectangle
+ * \param[in] xPos List of horizontal positions of the LSC grid nodes
+ * \param[in] yPos List of vertical positions of the LSC grid nodes
+ *
+ * Re-sample the lsc data for an \a analogCrop.
+ *
+ * Lsc data are generated at tuning time using a known sensor configuration.
+ * When a new streaming session is started, it might use a different sensor
+ * configuration for which the lsc tables need to be adjusted to.
+ *
+ * This function re-generates the lsc tables to adapt them to a new sensor
+ * configuration, specifically it re-samples the lsc data for a new \a
+ * analogCrop on a grid specified by \a xPos and \a yPos. Re-sampling of
+ * lsc data is only supported by polynomial-based lsc tables.
+ *
+ * \sa LscImplementation::resampleLscData
+ *
+ * \return 0 on success, a negative error code otherwise
+ */
+
+/**
+ * \fn LscAlgorithm::interpolateComponents
+ * \brief Interpolate the lsc tables for a given colour temperature
+ * \param[in] ct The colour temperature
+ *
+ * Lsc data are generated using different colour temperatures during the
+ * tuning phase.
+ *
+ * This function returns the interpolated lsc data for a given \a ct
+ * colour temperature.
+ *
+ * IPA algorithm can use this function to obtain a list of gains per-colour
+ * component to program their lsc engines with every time a significant enough
+ * change in colour temperature is detected.
+ *
+ * Calling this function is only valid after LscAlgorithm::configure() has been
+ * called. An empty components list is returned otherwise.
+ *
+ * \return The lsc gains table interpolated for temperature \a ct
+ */
+
+/**
+ * \fn LscAlgorithm::getComponents
+ *
+ * Return the map of lsc data per colour temperature.
+ *
+ * Calling this function is only valid after LscAlgorithm::configure() has been
+ * called. An empty components list is returned otherwise.
+ *
+ * \return The map of lsc gains tables per colour-temperature
+ */
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/lsc.h b/src/ipa/libipa/lsc.h
new file mode 100644
index 000000000000..8bfa3a7c4ecf
--- /dev/null
+++ b/src/ipa/libipa/lsc.h
@@ -0,0 +1,145 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Ideas on Board Oy
+ *
+ * libIPA Lsc algorithms
+ */
+
+#pragma once
+
+#include <memory>
+
+#include <libcamera/control_ids.h>
+
+#include "interpolator.h"
+#include "lsc_grid.h"
+#include "lsc_polynomial.h"
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(Lsc)
+
+namespace ipa {
+
+namespace lsc {
+
+struct ActiveState {
+	bool enabled;
+};
+
+struct FrameContext {
+	bool enabled;
+	bool update;
+};
+
+} /* namespace lsc */
+
+#ifndef __DOXYGEN__
+template<typename T>
+void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,
+		       std::vector<T> &dest, double lambda)
+{
+	ASSERT(a.size() == b.size());
+	dest.resize(a.size());
+	for (size_t i = 0; i < a.size(); i++)
+		dest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;
+}
+
+template<>
+void Interpolator<lsc::Components<uint16_t>>::
+	interpolate(const lsc::Components<uint16_t> &a,
+		    const lsc::Components<uint16_t> &b,
+		    lsc::Components<uint16_t> &dest,
+		    double lambda);
+#endif /* __DOXYGEN__ */
+
+class LscAlgorithmBase
+{
+public:
+	void queueRequest(lsc::ActiveState &state, lsc::FrameContext &context,
+			  const ControlList &controls);
+	void process(lsc::FrameContext &context, ControlList &metadata);
+};
+
+template<typename T, typename U>
+class LscAlgorithm : public LscAlgorithmBase
+{
+public:
+	int init(const ValueNode &tuningData, ControlInfoMap::Map &controls,
+		 LscDescriptor descriptor)
+	{
+		polynomial_ = false;
+
+		std::string type = tuningData["type"].get<std::string>("table");
+		if (type == "table") {
+			impl_ = std::make_unique<LscGrid<T, U>>();
+			LOG(Lsc, Debug) << "Using grid-based Lsc";
+		} else if (type == "polynomial") {
+			impl_ = std::make_unique<LscPolynomial<T, U>>();
+			polynomial_ = true;
+			LOG(Lsc, Debug) << "Using polynomial Lsc";
+		} else {
+			LOG(Lsc, Error) << "Unsupported Lsc algorithm '"
+					<< type << "'";
+			return -EINVAL;
+		}
+
+		const ValueNode &yamlSets = tuningData["sets"];
+		if (!yamlSets.isList()) {
+			LOG(Lsc, Error) << "'sets' parameter not found in tuning file";
+			return -EINVAL;
+		}
+
+		int ret = impl_->parseLscData(yamlSets, descriptor);
+		if (ret)
+			return ret;
+
+		controls[&controls::LensShadingCorrectionEnable] =
+			ControlInfo(false, true, true);
+
+		return 0;
+	}
+
+	int configure(lsc::ActiveState &state, const Rectangle &analogCrop,
+		      const std::vector<double> &xPos,
+		      const std::vector<double> &yPos)
+	{
+		LOG(Lsc, Debug) << "Sample Lsc data for " << analogCrop;
+		lsc::ComponentsMap<T> lscData =
+			impl_->resampleLscData(analogCrop, xPos, yPos);
+
+		/*
+		 * Retain a copy of the components table.
+		 *
+		 * We could avoid a copy here if getComponents() could
+		 * return sets_.data() but I wasn't able to work around the
+		 * compiler refusing it.
+		 */
+		lscData_ = lscData;
+
+		sets_.setData(std::move(lscData));
+		state.enabled = true;
+
+		return 0;
+	}
+
+	const lsc::Components<T> interpolateComponents(unsigned int ct)
+	{
+		return sets_.getInterpolated(ct);
+	}
+
+	const lsc::ComponentsMap<T> getComponents()
+	{
+		return lscData_;
+	}
+
+private:
+	std::unique_ptr<LscImplementation<T, U>> impl_;
+	Interpolator<lsc::Components<T>> sets_;
+	lsc::ComponentsMap<T> lscData_;
+	bool polynomial_;
+};
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/lsc_base.cpp b/src/ipa/libipa/lsc_base.cpp
new file mode 100644
index 000000000000..fba8d2f00d54
--- /dev/null
+++ b/src/ipa/libipa/lsc_base.cpp
@@ -0,0 +1,152 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Ideas on Board Oy
+ *
+ * Base classes and types for LSC algorithms implementations
+ */
+
+#include "lsc_base.h"
+
+/**
+ * \file lsc_base.h
+ * \brief Base types and definitions for LscImplementation class hierarchy
+ *
+ * Split the common classes and types definitions to a dedicated file to avoid
+ * circular inclusions.
+ *
+ * Both LscGrid and LscPolynomial inherit from LscImplementation and they are
+ * instantiated from lsc.h as the LscAlgorithm::init() function is there inlined.
+ *
+ * For this reason lsc.h needs to include lsc_grid.h and lsc_polynomial.h which
+ * need the LscImplementation class definition themselves.
+ *
+ * Split the LscImplementation interface definition to this file so that it can
+ * be included by both lsc_grid.h and lsc_polynomial.h, which are then included
+ * by lsc.h.
+ */
+
+namespace libcamera {
+
+namespace ipa {
+
+namespace lsc {
+
+/**
+ * \class Components
+ * \brief Associate a colour components with a list of gains
+ * \tparam T The type used to store the gain values when loaded from tuning file
+ *
+ * Lsc tables are defined as a list of gain values associated to a colour
+ * component.
+ *
+ * As different ISP support different colour components (usually 'r', 'gr',
+ * 'gb', 'b' or just 'r', 'g', 'b') this class associates a string
+ * identifier for the colour component to a list of gains of type \a T.
+ *
+ * Each key name shall match an entry in the tuning file and the type \a T
+ * shall match the size of the registers where gains are stored.
+ *
+ * The list of keys is provided to the LscAlgorithm class using \a
+ * LscDescriptor::keys.
+ */
+
+/**
+ * \class ComponentsMap
+ * \brief Associate a colour temperature to a lsc table
+ * \tparam T The type used to store the gain values when loaded from tuning file
+ *
+ * An lsc table is generated during the tuning phase for a specific light
+ * temperature, and a tuning file usually contains lsc tables generated for
+ * several different colour temperatures.
+ *
+ * This class associates an lsc table to the colour temperature used when the
+ * table has been generated.
+ */
+
+} /* namespace lsc */
+
+/**
+ * \struct LscDescriptor
+ * \brief Describe the ISP lsc engine
+ *
+ * \var LscDescriptor::keys
+ * \brief The list of colour components to which a list of gains is associated
+ * with in the tuning file. Used for parsing the tuning file
+ *
+ * \var LscDescriptor::numHCells
+ * \brief Number of horizontal cells of the ISP lsc grid. Used for validating
+ * the list of gains parsed from tuning file
+ *
+ * \var LscDescriptor::numVCells
+ * \brief Number of vertical cells of the ISP lsc grid. Used for validating
+ * the list of gains parsed from tuning file
+ *
+ * \var LscDescriptor::sensorSize
+ * \brief The physical sensor size. This is the largest frame size used to
+ * generate the lsc table. Only used by the polynomial lsc algorithm
+ */
+
+/**
+ * \class LscImplementation
+ * \brief Pure virtual base class for lsc algorithm implementations
+ * \tparam T The type used to store the gain values when loaded from tuning file
+ * \tparam U The fixedpoint format used to convert gain values generated by
+ * polynomial expansion to the register format
+ *
+ * Defines the interface for the lsc algorithm implementation. Currently
+ * implemented by LscGrid and LscPolynomial.
+ */
+
+/**
+ * \fn LscImplementation::~LscImplementation
+ * \brief Virtual class destructor
+ */
+
+/**
+ * \fn LscImplementation::parseLscData
+ * \brief Parse \a tuningData using \a descriptor
+ * \param[in] tuningData The tuning data
+ * \param[in] descriptor The lsc engine descriptor
+ *
+ * Parse the tuning file using the \a descriptor to identify the colour
+ * components in the tuning data and validate the size of the loaded gains
+ * tables.
+ *
+ * \return 0 on success, a negative error number otherwise
+ */
+
+/**
+ * \fn LscImplementation::resampleLscData
+ * \brief Re-sample the lsc components for \a cropRectangle
+ * \param[in] cropRectangle The sensor analogue crop rectangle
+ * \param[in] xPos List of horizontal positions of the lsc grid nodes
+ * \param[in] yPos List of vertical positions of the lsc grid nodes
+ *
+ * Lsc tables are expressed in two formats:
+ * - A list of gain values (LscGrid)
+ * - A radial polynomial (LscPolynomial)
+ *
+ * Grid-based lsc data are generated using an image at a fixed resolution and
+ * can't at the moment be re-sampled when a different resolution is used for a
+ * streaming session. Re-sampling a grid lsc table will return the same table
+ * as loaded from the tuning file.
+ *
+ * Polynomial are more flexible and can be re-sampled for a given resolution
+ * using a list of horizontal and vertical nodes that define the lsc grid.
+ * Polynomial lsc tables have to be re-sampled every time a new configuration is
+ * applied, as each streaming session might use a different sensor crop
+ * rectangle.
+ *
+ * \a cropRectangle represents the size of the frame on which the polynomial Lsc
+ * has to be re-sampled on.
+ *
+ * \a xPos and \a yPos represent the position of the grid nodes vertexes in
+ * the [0, 1] interval. In example an equally spaced grid of 16 nodes will have
+ * each segment of size 0.0625 and the list of nodes position will be
+ * [0, 0.0625, 0.125, 0.1875, ... , 1]. It is expected that the first position
+ * is 0 and the last position is 1.
+ */
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
new file mode 100644
index 000000000000..aecae305aa9c
--- /dev/null
+++ b/src/ipa/libipa/lsc_base.h
@@ -0,0 +1,59 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Ideas on Board Oy
+ *
+ * Base classes and types for LSC algorithms implementations
+ */
+
+#pragma once
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include <libcamera/geometry.h>
+
+#include "libcamera/internal/value_node.h"
+
+namespace libcamera {
+
+namespace ipa {
+
+namespace lsc {
+
+template<typename T>
+class Components : public std::map<std::string, std::vector<T>>
+{
+};
+
+template<typename T>
+class ComponentsMap : public std::map<unsigned int, Components<T>>
+{
+};
+
+} /* namespace lsc */
+
+struct LscDescriptor {
+	std::vector<std::string> keys;
+	unsigned int numHCells;
+	unsigned int numVCells;
+	Size sensorSize;
+};
+
+template<typename T, typename U>
+class LscImplementation
+{
+public:
+	virtual ~LscImplementation() = default;
+
+	virtual int parseLscData(const ValueNode &tuningData,
+				 const LscDescriptor &descriptor) = 0;
+
+	virtual lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
+						      const std::vector<double> &xPos,
+						      const std::vector<double> &yPos) = 0;
+};
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/lsc_grid.cpp b/src/ipa/libipa/lsc_grid.cpp
new file mode 100644
index 000000000000..878994acc8c9
--- /dev/null
+++ b/src/ipa/libipa/lsc_grid.cpp
@@ -0,0 +1,33 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas On Board
+ *
+ * Grid based lens shading correction
+ */
+
+#include "lsc_grid.h"
+
+/**
+ * \file lsc_grid.h
+ * \brief LscGrid class
+ */
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(LscGrid)
+
+namespace ipa {
+
+/**
+ * \class LscGrid
+ * \brief Grid based lsc algorithm implementation
+ *
+ * Grid based lsc algorithm implementation. The LscGrid class implements lsc
+ * support using tabular lsc data.
+ *
+ * \sa LscImplementation
+ * \sa LscAlgorithm
+ */
+
+} /* namespace ipa */
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/lsc_grid.h b/src/ipa/libipa/lsc_grid.h
new file mode 100644
index 000000000000..e92cde95d18d
--- /dev/null
+++ b/src/ipa/libipa/lsc_grid.h
@@ -0,0 +1,113 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas On Board
+ *
+ * Grid based lens shading correction
+ */
+
+#pragma once
+
+#include <vector>
+
+#include <libcamera/base/log.h>
+
+#include "lsc_base.h"
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(LscGrid)
+
+namespace ipa {
+
+template<typename T, typename U>
+class LscGrid : public LscImplementation<T, U>
+{
+public:
+	~LscGrid() {}
+
+	int parseLscData(const ValueNode &yamlSets,
+			 const LscDescriptor &descriptor) override
+	{
+		const auto &sets = yamlSets.asList();
+
+		for (const auto &yamlSet : sets) {
+			uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
+
+			int ret = parseLscComponent(yamlSet, ct, descriptor);
+			if (ret)
+				return ret;
+		}
+
+		if (lscData_.empty()) {
+			LOG(LscGrid, Error) << "Failed to load any sets";
+			return -EINVAL;
+		}
+
+		return 0;
+	}
+
+	lsc::ComponentsMap<T> resampleLscData([[maybe_unused]] const Rectangle &cropRectangle,
+					      [[maybe_unused]] const std::vector<double> &xPos,
+					      [[maybe_unused]] const std::vector<double> &yPos) override
+	{
+		/* No resampling for grid-based LSC algorithm. */
+		return lscData_;
+	}
+
+private:
+	int parseLscComponent(const ValueNode &yamlSet,
+			      unsigned int ct, const LscDescriptor &descriptor)
+	{
+		lsc::Components<T> component;
+		for (auto &k : descriptor.keys) {
+			auto [it, inserted] = component.emplace(
+				std::piecewise_construct,
+				std::forward_as_tuple(k.c_str()),
+				std::forward_as_tuple(parseTable(yamlSet,
+								 k.c_str(),
+								 descriptor.numHCells,
+								 descriptor.numVCells)));
+			if (!inserted || it->second.empty()) {
+				LOG(LscGrid, Error)
+					<< "Set " << k << " for color temperature "
+					<< ct << " is missing";
+				return -EINVAL;
+			}
+		}
+
+		auto [it, inserted] = lscData_.emplace(ct, component);
+		if (!inserted) {
+			LOG(LscGrid, Error)
+				<< "Multiple sets found for color temperature "
+				<< ct;
+			return -EINVAL;
+		}
+
+		return 0;
+	}
+
+	std::vector<T> parseTable(const ValueNode &tuningData,
+				  const char *prop, unsigned int numHCells,
+				  unsigned int numVCells)
+	{
+		unsigned int kLscNumSamples = numHCells * numVCells;
+
+		std::vector<T> table =
+			tuningData[prop].get<std::vector<T>>().value_or(utils::defopt);
+		if (table.size() != kLscNumSamples) {
+			LOG(LscGrid, Error)
+				<< "Invalid '" << prop << "' values: expected "
+				<< kLscNumSamples
+				<< " elements, got " << table.size();
+			return {};
+		}
+
+		return table;
+	}
+
+	lsc::ComponentsMap<T> lscData_;
+};
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
index f607d86c54c3..8d3f1883d6a5 100644
--- a/src/ipa/libipa/lsc_polynomial.cpp
+++ b/src/ipa/libipa/lsc_polynomial.cpp
@@ -1,12 +1,14 @@ 
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 /*
- * Copyright (C) 2024, Ideas On Board
+ * Copyright (C) 2026, Ideas On Board
  *
- * Polynomial class to represent lens shading correction
+ * Polynomial based lens shading correction
  */
 
 #include "lsc_polynomial.h"
 
+#include <assert.h>
+
 #include <libcamera/base/log.h>
 
 /**
@@ -21,7 +23,7 @@  LOG_DEFINE_CATEGORY(LscPolynomial)
 namespace ipa {
 
 /**
- * \class LscPolynomial
+ * \class Polynomial
  * \brief Class for handling even polynomials used in lens shading correction
  *
  * Shading artifacts of camera lenses can be modeled using even radial
@@ -31,7 +33,7 @@  namespace ipa {
  */
 
 /**
- * \fn LscPolynomial::LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
+ * \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
 		      double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
 		      double k4 = 0.0)
  * \brief Construct a polynomial using the given coefficients
@@ -45,7 +47,13 @@  namespace ipa {
  */
 
 /**
- * \fn LscPolynomial::sampleAtNormalizedPixelPos(double x, double y)
+ * \fn Polynomial::Polynomial(const Polynomial &other)
+ * \brief Construct a Polynomial by copy
+ * \param[in] other The Polynomial to copy construct from
+ */
+
+/**
+ * \fn Polynomial::sampleAtNormalizedPixelPos(double x, double y)
  * \brief Sample the polynomial at the given normalized pixel position
  *
  * This functions samples the polynomial at the given pixel position divided by
@@ -55,9 +63,21 @@  namespace ipa {
  * \param y y position in normalized coordinates
  * \return The sampled value
  */
+double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
+{
+	double dx = x - cnx_;
+	double dy = y - cny_;
+	double r = sqrt(dx * dx + dy * dy);
+	double res = 1.0;
+
+	for (unsigned int i = 0; i < coefficients_.size(); i++)
+		res += coefficients_[i] * std::pow(r, (i + 1) * 2);
+
+	return res;
+}
 
 /**
- * \fn LscPolynomial::getM()
+ * \fn Polynomial::getM()
  * \brief Get the value m as described in the dng specification
  *
  * Returns m according to dng spec. m represents the Euclidean distance
@@ -66,9 +86,18 @@  namespace ipa {
  *
  * \return The sampled value
  */
+double Polynomial::getM() const
+{
+	double cpx = imageSize_.width * cx_;
+	double cpy = imageSize_.height * cy_;
+	double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
+	double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
+
+	return sqrt(mx * mx + my * my);
+}
 
 /**
- * \fn LscPolynomial::setReferenceImageSize(const Size &size)
+ * \fn Polynomial::setReferenceImageSize(const Size &size)
  * \brief Set the reference image size
  *
  * Set the reference image size that is used for subsequent calls to getM() and
@@ -76,6 +105,92 @@  namespace ipa {
  *
  * \param size The size of the reference image
  */
+void Polynomial::setReferenceImageSize(const Size &size)
+{
+	assert(!size.isNull());
+	imageSize_ = size;
+
+	/* Calculate normalized centers */
+	double m = getM();
+	cnx_ = (size.width * cx_) / m;
+	cny_ = (size.height * cy_) / m;
+}
+
+/**
+ * \class LscPolynomialBase
+ * \brief Base class for LscPolynomial
+ *
+ * Base class for LscPolynomial for non-templated functions.
+ */
+
+/**
+ * \brief Parse polynomial lsc data
+ * \param[in] yamlSets The tuning file content
+ * \param[in] descriptor The lsc engine descriptor
+ *
+ * Parse the lsc data in polyomial form from the \a yamlSet tuning data.
+ */
+int LscPolynomialBase::parseLscData(const ValueNode &yamlSets,
+				    const LscDescriptor &descriptor)
+{
+	const auto &sets = yamlSets.asList();
+	for (const auto &yamlSet : sets) {
+		uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
+
+		Components components;
+		for (auto &k : descriptor.keys) {
+			auto polynomial = yamlSet[k.c_str()].get<Polynomial>();
+			if (!polynomial) {
+				LOG(LscPolynomial, Error)
+					<< "Missing polynomial for component "
+					<< k;
+				return -EINVAL;
+			}
+
+			auto [it, inserted] =
+				components.emplace(std::piecewise_construct,
+						   std::forward_as_tuple(k.c_str()),
+						   std::forward_as_tuple(*polynomial));
+
+			it->second.setReferenceImageSize(descriptor.sensorSize);
+		}
+
+		auto [it, inserted] = lscData_.emplace(ct, components);
+		if (!inserted) {
+			LOG(LscPolynomial, Error)
+				<< "Multiple sets found for "
+				<< "color temperature " << ct;
+			return -EINVAL;
+		}
+	}
+
+	if (lscData_.empty()) {
+		LOG(LscPolynomial, Error) << "Failed to load any sets";
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+/**
+ * \var LscPolynomialBase::lscData_
+ * \brief The polynomial lsc data
+ *
+ * Maps colour temperatures to per-colour radial polynomial definitions.
+ */
+
+/**
+ * \class LscPolynomial
+ * \brief Radial Polynomial lsc algorithm implementation
+ *
+ * Polynomial-based lsc algorithm implementation. The LscPolynomial class
+ * implements lsc support using Polynomial to represent the shading artifacts
+ * map.
+ *
+ * \sa LscImplementation
+ * \sa LscAlgorithm
+ *
+ */
 
-} // namespace ipa
-} // namespace libcamera
+} /* namespace ipa */
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/lsc_polynomial.h b/src/ipa/libipa/lsc_polynomial.h
index d7d9ae42e360..72cca591f260 100644
--- a/src/ipa/libipa/lsc_polynomial.h
+++ b/src/ipa/libipa/lsc_polynomial.h
@@ -1,15 +1,17 @@ 
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 /*
- * Copyright (C) 2024, Ideas On Board
+ * Copyright (C) 2026, Ideas On Board
  *
- * Helper for radial polynomial used in lens shading correction.
+ * Polynomial based lens shading correction
  */
+
 #pragma once
 
 #include <algorithm>
 #include <array>
-#include <assert.h>
 #include <cmath>
+#include <optional>
+#include <tuple>
 
 #include <libcamera/base/log.h>
 #include <libcamera/base/span.h>
@@ -18,64 +20,117 @@ 
 
 #include "libcamera/internal/value_node.h"
 
+#include "lsc_base.h"
+
 namespace libcamera {
 
 LOG_DECLARE_CATEGORY(LscPolynomial)
 
 namespace ipa {
 
-class LscPolynomial
+class Polynomial
 {
 public:
-	LscPolynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
-		      double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
-		      double k4 = 0.0)
+	Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
+		   double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
+		   double k4 = 0.0)
 		: cx_(cx), cy_(cy), cnx_(0), cny_(0),
 		  coefficients_({ k0, k1, k2, k3, k4 })
 	{
 	}
 
-	double sampleAtNormalizedPixelPos(double x, double y) const
+	Polynomial(const Polynomial &other) = default;
+
+	double sampleAtNormalizedPixelPos(double x, double y) const;
+	double getM() const;
+	void setReferenceImageSize(const Size &size);
+
+private:
+	double cx_;
+	double cy_;
+	double cnx_;
+	double cny_;
+	std::array<double, 5> coefficients_;
+	Size imageSize_;
+};
+
+class LscPolynomialBase
+{
+private:
+	using Components = std::map<std::string, Polynomial>;
+	using ComponentsMap = std::map<unsigned int, Components>;
+
+public:
+	int parseLscData(const ValueNode &yamlSets,
+			 const LscDescriptor &descriptor);
+
+protected:
+	ComponentsMap lscData_;
+};
+
+template<typename T, typename U>
+class LscPolynomial : public LscPolynomialBase, public LscImplementation<T, U>
+{
+public:
+	~LscPolynomial() {}
+
+	int parseLscData(const ValueNode &yamlSets,
+			 const LscDescriptor &descriptor) override
 	{
-		double dx = x - cnx_;
-		double dy = y - cny_;
-		double r = sqrt(dx * dx + dy * dy);
-		double res = 1.0;
-		for (unsigned int i = 0; i < coefficients_.size(); i++) {
-			res += coefficients_[i] * std::pow(r, (i + 1) * 2);
-		}
-		return res;
+		return LscPolynomialBase::parseLscData(yamlSets, descriptor);
 	}
 
-	double getM() const
+	lsc::ComponentsMap<T> resampleLscData(const Rectangle &cropRectangle,
+					      const std::vector<double> &xPos,
+					      const std::vector<double> &yPos) override
 	{
-		double cpx = imageSize_.width * cx_;
-		double cpy = imageSize_.height * cy_;
-		double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
-		double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
+		lsc::ComponentsMap<T> components;
 
-		return sqrt(mx * mx + my * my);
-	}
+		for (auto &[t, c] : lscData_) {
+			lsc::Components<T> comp;
 
-	void setReferenceImageSize(const Size &size)
-	{
-		assert(!size.isNull());
-		imageSize_ = size;
+			for (auto &[k, p] : c) {
+				comp.emplace(std::piecewise_construct,
+					     std::forward_as_tuple(k),
+					     std::forward_as_tuple(samplePolynomial(p, xPos, yPos,
+										    cropRectangle)));
+			}
 
-		/* Calculate normalized centers */
-		double m = getM();
-		cnx_ = (size.width * cx_) / m;
-		cny_ = (size.height * cy_) / m;
+			components[t] = comp;
+		}
+
+		return components;
 	}
 
 private:
-	double cx_;
-	double cy_;
-	double cnx_;
-	double cny_;
-	std::array<double, 5> coefficients_;
+	std::vector<T> samplePolynomial(const Polynomial &poly,
+					Span<const double> xPositions,
+					Span<const double> yPositions,
+					const Rectangle &cropRectangle)
 
-	Size imageSize_;
+	{
+		double m = poly.getM();
+		double x0 = cropRectangle.x / m;
+		double y0 = cropRectangle.y / m;
+		double w = cropRectangle.width / m;
+		double h = cropRectangle.height / m;
+		std::vector<T> samples;
+
+		samples.reserve(xPositions.size() * yPositions.size());
+
+		for (double y : yPositions) {
+			for (double x : xPositions) {
+				double xp = x0 + x * w;
+				double yp = y0 + y * h;
+				float sample = static_cast<float>
+						(poly.sampleAtNormalizedPixelPos(xp, yp));
+
+				samples.push_back(U(sample).quantized());
+			}
+		}
+
+		return samples;
+	}
 };
 
 } /* namespace ipa */
@@ -83,8 +138,8 @@  private:
 #ifndef __DOXYGEN__
 
 template<>
-struct ValueNode::Accessor<ipa::LscPolynomial> {
-	std::optional<ipa::LscPolynomial> get(const ValueNode &obj) const
+struct ValueNode::Accessor<ipa::Polynomial> {
+	std::optional<ipa::Polynomial> get(const ValueNode &obj) const
 	{
 		std::optional<double> cx = obj["cx"].get<double>();
 		std::optional<double> cy = obj["cy"].get<double>();
@@ -98,7 +153,7 @@  struct ValueNode::Accessor<ipa::LscPolynomial> {
 			LOG(LscPolynomial, Error)
 				<< "Polynomial is missing a parameter";
 
-		return ipa::LscPolynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);
+		return ipa::Polynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);
 	}
 };
 
diff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build
index edf8eabd8b78..da6ea0c5e130 100644
--- a/src/ipa/libipa/meson.build
+++ b/src/ipa/libipa/meson.build
@@ -14,6 +14,9 @@  libipa_headers = files([
     'fixedpoint.h',
     'histogram.h',
     'interpolator.h',
+    'lsc.h',
+    'lsc_base.h',
+    'lsc_grid.h',
     'lsc_polynomial.h',
     'lux.h',
     'module.h',
@@ -36,6 +39,8 @@  libipa_sources = files([
     'fixedpoint.cpp',
     'histogram.cpp',
     'interpolator.cpp',
+    'lsc.cpp',
+    'lsc_grid.cpp',
     'lsc_polynomial.cpp',
     'lux.cpp',
     'module.cpp',
diff --git a/src/ipa/rkisp1/algorithms/lsc.cpp b/src/ipa/rkisp1/algorithms/lsc.cpp
index 25f7c512b35c..51076737ffa5 100644
--- a/src/ipa/rkisp1/algorithms/lsc.cpp
+++ b/src/ipa/rkisp1/algorithms/lsc.cpp
@@ -2,7 +2,7 @@ 
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * RkISP1 Lens Shading Correction control
+ * RkISP1 Lens Shading Correction algorithm
  */
 
 #include "lsc.h"
@@ -14,56 +14,17 @@ 
 #include <libcamera/base/log.h>
 #include <libcamera/base/utils.h>
 
-#include "libcamera/internal/value_node.h"
-
-#include "libipa/lsc_polynomial.h"
-#include "linux/rkisp1-config.h"
-
 /**
  * \file lsc.h
  */
 
 namespace libcamera {
 
-namespace ipa {
-
-template<typename T>
-void interpolateVector(const std::vector<T> &a, const std::vector<T> &b,
-		       std::vector<T> &dest, double lambda)
-{
-	ASSERT(a.size() == b.size());
-	dest.resize(a.size());
-	for (size_t i = 0; i < a.size(); i++)
-		dest[i] = a[i] * (1.0 - lambda) + b[i] * lambda;
-}
-
-template<>
-void Interpolator<rkisp1::algorithms::LensShadingCorrection::Components>::
-	interpolate(const rkisp1::algorithms::LensShadingCorrection::Components &a,
-		    const rkisp1::algorithms::LensShadingCorrection::Components &b,
-		    rkisp1::algorithms::LensShadingCorrection::Components &dest,
-		    double lambda)
-{
-	interpolateVector(a.r, b.r, dest.r, lambda);
-	interpolateVector(a.gr, b.gr, dest.gr, lambda);
-	interpolateVector(a.gb, b.gb, dest.gb, lambda);
-	interpolateVector(a.b, b.b, dest.b, lambda);
-}
-
-} /* namespace ipa */
-
 namespace ipa::rkisp1::algorithms {
 
 /**
  * \class LensShadingCorrection
  * \brief RkISP1 Lens Shading Correction control
- *
- * Due to the optical characteristics of the lens, the light intensity received
- * by the sensor is not uniform.
- *
- * The Lens Shading Correction algorithm applies multipliers to all pixels
- * to compensate for the lens shading effect. The coefficients are
- * specified in a downscaled table in the YAML tuning file.
  */
 
 LOG_DEFINE_CATEGORY(RkISP1Lsc)
@@ -72,265 +33,20 @@  namespace {
 
 constexpr int kColourTemperatureQuantization = 10;
 
-class LscPolynomialShadingDescriptor : public LensShadingCorrection::ShadingDescriptor
-{
-public:
-	LscPolynomialShadingDescriptor(const LscPolynomial &pr, const LscPolynomial &pgr,
-				       const LscPolynomial &pgb, const LscPolynomial &pb)
-		: pr_(pr), pgr_(pgr), pgb_(pgb), pb_(pb)
-	{
-	}
-
-	LensShadingCorrection::Components sampleForCrop(const Rectangle &cropRectangle,
-							Span<const double> xSizes,
-							Span<const double> ySizes) override;
-
-private:
-	std::vector<uint16_t> samplePolynomial(const LscPolynomial &poly,
-					       Span<const double> xPositions,
-					       Span<const double> yPositions,
-					       const Rectangle &cropRectangle);
-
-	std::vector<double> sizesListToPositions(Span<const double> sizes);
-
-	LscPolynomial pr_;
-	LscPolynomial pgr_;
-	LscPolynomial pgb_;
-	LscPolynomial pb_;
-};
-
-LensShadingCorrection::Components
-LscPolynomialShadingDescriptor::sampleForCrop(const Rectangle &cropRectangle,
-					      Span<const double> xSizes,
-					      Span<const double> ySizes)
-{
-	std::vector<double> xPos = sizesListToPositions(xSizes);
-	std::vector<double> yPos = sizesListToPositions(ySizes);
-
-	return {
-		.r = samplePolynomial(pr_, xPos, yPos, cropRectangle),
-		.gr = samplePolynomial(pgr_, xPos, yPos, cropRectangle),
-		.gb = samplePolynomial(pgb_, xPos, yPos, cropRectangle),
-		.b = samplePolynomial(pb_, xPos, yPos, cropRectangle)
-	};
-}
-
-std::vector<uint16_t>
-LscPolynomialShadingDescriptor::samplePolynomial(const LscPolynomial &poly,
-						 Span<const double> xPositions,
-						 Span<const double> yPositions,
-						 const Rectangle &cropRectangle)
-{
-	double m = poly.getM();
-	double x0 = cropRectangle.x / m;
-	double y0 = cropRectangle.y / m;
-	double w = cropRectangle.width / m;
-	double h = cropRectangle.height / m;
-	std::vector<uint16_t> samples;
-
-	samples.reserve(xPositions.size() * yPositions.size());
-
-	for (double y : yPositions) {
-		for (double x : xPositions) {
-			double xp = x0 + x * w;
-			double yp = y0 + y * h;
-			/*
-			 * The hardware uses 2.10 fixed point format and limits
-			 * the legal values to [1..3.999]. Scale and clamp the
-			 * sampled value accordingly.
-			 */
-			int v = static_cast<int>(
-				poly.sampleAtNormalizedPixelPos(xp, yp) *
-				1024);
-			v = std::clamp(v, 1024, 4095);
-			samples.push_back(v);
-		}
-	}
-	return samples;
-}
-
-/*
- * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left
- * quadrant of the grid. This is then mirrored in hardware to the other
- * quadrants. See parseSizes() for further details. For easier handling, this
- * function converts the cell sizes of half the grid to a list of position of
- * the whole grid (on one axis). Example:
- *
- * input:   | 0.2 | 0.3 |
- * output: 0.0   0.2   0.5   0.8   1.0
- */
-std::vector<double>
-LscPolynomialShadingDescriptor::sizesListToPositions(Span<const double> sizes)
-{
-	const int half = sizes.size();
-	std::vector<double> positions(half * 2 + 1);
-	double x = 0.0;
-
-	positions[half] = 0.5;
-	for (int i = 1; i <= half; i++) {
-		x += sizes[half - i];
-		positions[half - i] = 0.5 - x;
-		positions[half + i] = 0.5 + x;
-	}
-
-	return positions;
-}
-
-class LscPolynomialLoader
-{
-public:
-	LscPolynomialLoader(const Size &sensorSize)
-		: sensorSize_(sensorSize)
-	{
-	}
-
-	int parseLscData(const ValueNode &yamlSets,
-			 LensShadingCorrection::ShadingDescriptorMap &lscData);
-
-private:
-	Size sensorSize_;
-};
-
-int LscPolynomialLoader::parseLscData(const ValueNode &yamlSets,
-				      LensShadingCorrection::ShadingDescriptorMap &lscData)
+unsigned int quantize(unsigned int value, unsigned int step)
 {
-	const auto &sets = yamlSets.asList();
-	for (const auto &yamlSet : sets) {
-		std::optional<LscPolynomial> pr, pgr, pgb, pb;
-		uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
-
-		if (lscData.count(ct)) {
-			LOG(RkISP1Lsc, Error)
-				<< "Multiple sets found for "
-				<< "color temperature " << ct;
-			return -EINVAL;
-		}
-
-		pr = yamlSet["r"].get<LscPolynomial>();
-		pgr = yamlSet["gr"].get<LscPolynomial>();
-		pgb = yamlSet["gb"].get<LscPolynomial>();
-		pb = yamlSet["b"].get<LscPolynomial>();
-
-		if (!(pr || pgr || pgb || pb)) {
-			LOG(RkISP1Lsc, Error)
-				<< "Failed to parse polynomial for "
-				<< "colour temperature " << ct;
-			return -EINVAL;
-		}
-
-		pr->setReferenceImageSize(sensorSize_);
-		pgr->setReferenceImageSize(sensorSize_);
-		pgb->setReferenceImageSize(sensorSize_);
-		pb->setReferenceImageSize(sensorSize_);
-
-		lscData.emplace(
-			ct, std::make_unique<LscPolynomialShadingDescriptor>(
-				    *pr, *pgr, *pgb, *pb));
-	}
-
-	if (lscData.empty()) {
-		LOG(RkISP1Lsc, Error) << "Failed to load any sets";
-		return -EINVAL;
-	}
-
-	return 0;
+	return std::lround(value / static_cast<double>(step)) * step;
 }
 
-class LscTableShadingDescriptor : public LensShadingCorrection::ShadingDescriptor
-{
-public:
-	LscTableShadingDescriptor(LensShadingCorrection::Components components)
-		: lscData_(std::move(components))
-	{
-	}
-
-	LensShadingCorrection::Components
-	sampleForCrop([[maybe_unused]] const Rectangle &cropRectangle,
-		      [[maybe_unused]] Span<const double> xSizes,
-		      [[maybe_unused]] Span<const double> ySizes) override
-	{
-		LOG(RkISP1Lsc, Warning)
-			<< "Tabular LSC data doesn't support resampling";
-		return lscData_;
-	}
-
-private:
-	LensShadingCorrection::Components lscData_;
-};
-
-class LscTableLoader
-{
-public:
-	int parseLscData(const ValueNode &yamlSets,
-			 LensShadingCorrection::ShadingDescriptorMap &lscData);
-
-private:
-	std::vector<uint16_t> parseTable(const ValueNode &tuningData,
-					 const char *prop);
-};
-
-int LscTableLoader::parseLscData(const ValueNode &yamlSets,
-				 LensShadingCorrection::ShadingDescriptorMap &lscData)
-{
-	const auto &sets = yamlSets.asList();
-
-	for (const auto &yamlSet : sets) {
-		uint32_t ct = yamlSet["ct"].get<uint32_t>(0);
-
-		if (lscData.count(ct)) {
-			LOG(RkISP1Lsc, Error)
-				<< "Multiple sets found for color temperature "
-				<< ct;
-			return -EINVAL;
-		}
-
-		LensShadingCorrection::Components set;
-		set.r = parseTable(yamlSet, "r");
-		set.gr = parseTable(yamlSet, "gr");
-		set.gb = parseTable(yamlSet, "gb");
-		set.b = parseTable(yamlSet, "b");
-
-		if (set.r.empty() || set.gr.empty() ||
-		    set.gb.empty() || set.b.empty()) {
-			LOG(RkISP1Lsc, Error)
-				<< "Set for color temperature " << ct
-				<< " is missing tables";
-			return -EINVAL;
-		}
-
-		lscData.emplace(
-			ct, std::make_unique<LscTableShadingDescriptor>(std::move(set)));
-	}
-
-	if (lscData.empty()) {
-		LOG(RkISP1Lsc, Error) << "Failed to load any sets";
-		return -EINVAL;
-	}
-
-	return 0;
-}
+} /* namespace */
 
-std::vector<uint16_t> LscTableLoader::parseTable(const ValueNode &tuningData,
-						 const char *prop)
+LensShadingCorrection::LensShadingCorrection()
+	: lastAppliedCt_(0), lastAppliedQuantizedCt_(0)
 {
-	static constexpr unsigned int kLscNumSamples =
-		RKISP1_CIF_ISP_LSC_SAMPLES_MAX * RKISP1_CIF_ISP_LSC_SAMPLES_MAX;
-
-	std::vector<uint16_t> table =
-		tuningData[prop].get<std::vector<uint16_t>>().value_or(utils::defopt);
-	if (table.size() != kLscNumSamples) {
-		LOG(RkISP1Lsc, Error)
-			<< "Invalid '" << prop << "' values: expected "
-			<< kLscNumSamples
-			<< " elements, got " << table.size();
-		return {};
-	}
-
-	return table;
 }
 
-std::vector<double> parseSizes(const ValueNode &tuningData,
-			       const char *prop)
+std::vector<double> LensShadingCorrection::parseSizes(const ValueNode &tuningData,
+						      const char *prop)
 {
 	std::vector<double> sizes =
 		tuningData[prop].get<std::vector<double>>().value_or(utils::defopt);
@@ -359,22 +75,36 @@  std::vector<double> parseSizes(const ValueNode &tuningData,
 	return sizes;
 }
 
-unsigned int quantize(unsigned int value, unsigned int step)
+/*
+ * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left
+ * quadrant of the grid. This is then mirrored in hardware to the other
+ * quadrants. See parseSizes() for further details. For easier handling, this
+ * function converts the cell sizes of half the grid to a list of position of
+ * the whole grid (on one axis). Example:
+ *
+ * input:   | 0.2 | 0.3 |
+ * output: 0.0   0.2   0.5   0.8   1.0
+ */
+std::vector<double> LensShadingCorrection::sizesToPositions(Span<const double> sizes)
 {
-	return std::lround(value / static_cast<double>(step)) * step;
-}
+	const int half = sizes.size();
+	std::vector<double> positions(half * 2 + 1);
+	double x = 0.0;
 
-} /* namespace */
+	positions[half] = 0.5;
+	for (int i = 1; i <= half; i++) {
+		x += sizes[half - i];
+		positions[half - i] = 0.5 - x;
+		positions[half + i] = 0.5 + x;
+	}
 
-LensShadingCorrection::LensShadingCorrection()
-	: lastAppliedCt_(0), lastAppliedQuantizedCt_(0)
-{
+	return positions;
 }
 
 /**
  * \copydoc libcamera::ipa::Algorithm::init
  */
-int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
+int LensShadingCorrection::init(IPAContext &context,
 				const ValueNode &tuningData)
 {
 	xSize_ = parseSizes(tuningData, "x-size");
@@ -383,56 +113,27 @@  int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
 	if (xSize_.empty() || ySize_.empty())
 		return -EINVAL;
 
-	/* Get all defined sets to apply. */
-	const ValueNode &yamlSets = tuningData["sets"];
-	if (!yamlSets.isList()) {
-		LOG(RkISP1Lsc, Error)
-			<< "'sets' parameter not found in tuning file";
-		return -EINVAL;
-	}
-
-	ShadingDescriptorMap lscData;
-	int ret = 0;
-
-	std::string type = tuningData["type"].get<std::string>("table");
-	if (type == "table") {
-		LOG(RkISP1Lsc, Debug) << "Loading tabular LSC data.";
-		auto loader = LscTableLoader();
-		ret = loader.parseLscData(yamlSets, lscData);
-	} else if (type == "polynomial") {
-		LOG(RkISP1Lsc, Debug) << "Loading polynomial LSC data.";
-		/*
-		 * \todo: Most likely the reference frame should be native_size.
-		 * Let's wait how the internal discussions progress.
-		 */
-		auto loader = LscPolynomialLoader(context.sensorInfo.activeAreaSize);
-		ret = loader.parseLscData(yamlSets, lscData);
-	} else {
-		LOG(RkISP1Lsc, Error) << "Unsupported LSC data type '"
-				      << type << "'";
-		ret = -EINVAL;
-	}
-
-	if (ret)
-		return ret;
+	xPos_ = sizesToPositions(xSize_);
+	yPos_ = sizesToPositions(ySize_);
 
-	context.ctrlMap[&controls::LensShadingCorrectionEnable] =
-		ControlInfo(false, true, true);
-
-	shadingDescriptors_ = std::move(lscData);
-
-	return 0;
+	return lscAlgo_.init(tuningData, context.ctrlMap, {
+				.keys = { "r", "gr", "gb", "b" },
+				.numHCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,
+				.numVCells = RKISP1_CIF_ISP_LSC_SAMPLES_MAX,
+				.sensorSize = context.sensorInfo.activeAreaSize
+			     });
 }
 
 /**
  * \copydoc libcamera::ipa::Algorithm::configure
  */
 int LensShadingCorrection::configure(IPAContext &context,
-				     [[maybe_unused]] const IPACameraSensorInfo &configInfo)
+				     const IPACameraSensorInfo &configInfo)
 {
 	const Size &size = context.configuration.sensor.size;
 	Size totalSize{};
 
+	/* Calculate gradients. */
 	for (unsigned int i = 0; i < RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE; ++i) {
 		xSizes_[i] = xSize_[i] * size.width;
 		ySizes_[i] = ySize_[i] * size.height;
@@ -455,16 +156,8 @@  int LensShadingCorrection::configure(IPAContext &context,
 		yGrad_[i] = std::round(32768 / ySizes_[i]);
 	}
 
-	LOG(RkISP1Lsc, Debug) << "Sample LSC data for " << configInfo.analogCrop;
-	std::map<unsigned int, LensShadingCorrection::Components> shadingData;
-	for (const auto &[t, descriptor] : shadingDescriptors_)
-		shadingData[t] = descriptor->sampleForCrop(configInfo.analogCrop,
-							   xSize_, ySize_);
-
-	sets_.setData(std::move(shadingData));
-
-	context.activeState.lsc.enabled = true;
-	return 0;
+	return lscAlgo_.configure(context.activeState.lsc, configInfo.analogCrop,
+				  xPos_, yPos_);
 }
 
 void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)
@@ -476,12 +169,16 @@  void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)
 }
 
 void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
-				      const Components &set)
+				      const ipa::lsc::Components<uint16_t> &set)
 {
-	std::copy(set.r.begin(), set.r.end(), &config.r_data_tbl[0][0]);
-	std::copy(set.gr.begin(), set.gr.end(), &config.gr_data_tbl[0][0]);
-	std::copy(set.gb.begin(), set.gb.end(), &config.gb_data_tbl[0][0]);
-	std::copy(set.b.begin(), set.b.end(), &config.b_data_tbl[0][0]);
+	const auto &r = set.at("r");
+	std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0]);
+	const auto &gr = set.at("gr");
+	std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0]);
+	const auto &gb = set.at("gb");
+	std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0]);
+	const auto &b = set.at("b");
+	std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0]);
 }
 
 /**
@@ -492,19 +189,8 @@  void LensShadingCorrection::queueRequest(IPAContext &context,
 					 IPAFrameContext &frameContext,
 					 const ControlList &controls)
 {
-	auto &lsc = context.activeState.lsc;
-
-	const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);
-	if (lscEnable && *lscEnable != lsc.enabled) {
-		lsc.enabled = *lscEnable;
-
-		LOG(RkISP1Lsc, Debug)
-			<< (lsc.enabled ? "Enabling" : "Disabling") << " Lsc";
-
-		frameContext.lsc.update = true;
-	}
-
-	frameContext.lsc.enabled = lsc.enabled;
+	lscAlgo_.queueRequest(context.activeState.lsc, frameContext.lsc,
+			      controls);
 }
 
 /**
@@ -542,7 +228,7 @@  void LensShadingCorrection::prepare([[maybe_unused]] IPAContext &context,
 
 	setParameters(*config);
 
-	const Components &set = sets_.getInterpolated(quantizedCt);
+	const auto &set = lscAlgo_.interpolateComponents(quantizedCt);
 	copyTable(*config, set);
 
 	lastAppliedCt_ = ct;
@@ -562,7 +248,7 @@  void LensShadingCorrection::process([[maybe_unused]] IPAContext &context,
 				    [[maybe_unused]] const rkisp1_stat_buffer *stats,
 				    ControlList &metadata)
 {
-	metadata.set(controls::LensShadingCorrectionEnable, frameContext.lsc.enabled);
+	lscAlgo_.process(frameContext.lsc, metadata);
 }
 
 REGISTER_IPA_ALGORITHM(LensShadingCorrection, "LensShadingCorrection")
diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
index 0a256e225327..7c656d95acfb 100644
--- a/src/ipa/rkisp1/algorithms/lsc.h
+++ b/src/ipa/rkisp1/algorithms/lsc.h
@@ -2,17 +2,23 @@ 
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * RkISP1 Lens Shading Correction control
+ * RkISP1 Lens Shading Correction algorithm
  */
 
 #pragma once
 
-#include <map>
-#include <memory>
+#include <vector>
 
-#include "libipa/interpolator.h"
+#include <linux/rkisp1-config.h>
+
+#include "libcamera/internal/value_node.h"
+
+#include "libipa/fixedpoint.h"
+#include "libipa/lsc.h"
 
 #include "algorithm.h"
+#include "ipa_context.h"
+#include "params.h"
 
 namespace libcamera {
 
@@ -37,39 +43,28 @@  public:
 		     const rkisp1_stat_buffer *stats,
 		     ControlList &metadata) override;
 
-	struct Components {
-		std::vector<uint16_t> r;
-		std::vector<uint16_t> gr;
-		std::vector<uint16_t> gb;
-		std::vector<uint16_t> b;
-	};
-
-	class ShadingDescriptor
-	{
-	public:
-		virtual ~ShadingDescriptor() = default;
-		virtual Components sampleForCrop(const Rectangle &cropRectangle,
-						 Span<const double> xSizes,
-						 Span<const double> ySizes) = 0;
-	};
-
-	using ShadingDescriptorMap = std::map<unsigned int, std::unique_ptr<ShadingDescriptor>>;
-
 private:
+	std::vector<double> parseSizes(const ValueNode &tuningData,
+				       const char *prop);
+	std::vector<double> sizesToPositions(Span<const double> sizes);
+
 	void setParameters(rkisp1_cif_isp_lsc_config &config);
-	void copyTable(rkisp1_cif_isp_lsc_config &config, const Components &set0);
+	void copyTable(rkisp1_cif_isp_lsc_config &config,
+		       const ipa::lsc::Components<uint16_t> &set0);
 
-	ShadingDescriptorMap shadingDescriptors_;
-	ipa::Interpolator<Components> sets_;
 	std::vector<double> xSize_;
 	std::vector<double> ySize_;
 	uint16_t xGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
 	uint16_t yGrad_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
 	uint16_t xSizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
 	uint16_t ySizes_[RKISP1_CIF_ISP_LSC_SECTORS_TBL_SIZE];
+	std::vector<double> xPos_;
+	std::vector<double> yPos_;
 
 	unsigned int lastAppliedCt_;
 	unsigned int lastAppliedQuantizedCt_;
+
+	LscAlgorithm<uint16_t, UQ<2, 10>> lscAlgo_;
 };
 
 } /* namespace ipa::rkisp1::algorithms */
diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
index cd97e10bcf2b..005f4102b4f6 100644
--- a/src/ipa/rkisp1/ipa_context.h
+++ b/src/ipa/rkisp1/ipa_context.h
@@ -30,6 +30,7 @@ 
 #include "libipa/ccm.h"
 #include "libipa/fc_queue.h"
 #include "libipa/fixedpoint.h"
+#include "libipa/lsc.h"
 
 namespace libcamera {
 
@@ -138,9 +139,7 @@  struct IPAActiveState {
 		double strength;
 	} wdr;
 
-	struct {
-		bool enabled;
-	} lsc;
+	ipa::lsc::ActiveState lsc;
 };
 
 struct IPAFrameContext : public FrameContext {
@@ -213,10 +212,7 @@  struct IPAFrameContext : public FrameContext {
 		double gain;
 	} wdr;
 
-	struct {
-		bool enabled;
-		bool update;
-	} lsc;
+	ipa::lsc::FrameContext lsc;
 };
 
 struct IPAContext {