[v3,16/35] ipa: libipa: lsc_polynomial: Move LscPolynomial from RkISP1
diff mbox series

Message ID 20260706-libipa-algorithms-v3-16-968757b038bb@ideasonboard.com
State Superseded
Headers show
Series
  • ipa: libipa: Introduce libipa algorithms
Related show

Commit Message

Jacopo Mondi July 6, 2026, 8:01 a.m. UTC
Move the LscPolynomial class to libipa from RkISP1.

Rename the class from LscPolynomialImpl to a simpler LscPolynomial.
The class is copied verbatim, only documntation has been added.

Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
---
 src/ipa/libipa/lsc_polynomial.cpp | 179 +++++++++++++++++++++++++++++++++++++-
 src/ipa/libipa/lsc_polynomial.h   |  46 ++++++++--
 src/ipa/rkisp1/algorithms/lsc.cpp | 162 +---------------------------------
 src/ipa/rkisp1/algorithms/lsc.h   |   1 -
 4 files changed, 218 insertions(+), 170 deletions(-)

Comments

Kieran Bingham July 6, 2026, 6:34 p.m. UTC | #1
Quoting Jacopo Mondi (2026-07-06 09:01:36)
> Move the LscPolynomial class to libipa from RkISP1.
> 
> Rename the class from LscPolynomialImpl to a simpler LscPolynomial.
> The class is copied verbatim, only documntation has been added.

s/documntation/documentation/

> 
> Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> ---
>  src/ipa/libipa/lsc_polynomial.cpp | 179 +++++++++++++++++++++++++++++++++++++-
>  src/ipa/libipa/lsc_polynomial.h   |  46 ++++++++--
>  src/ipa/rkisp1/algorithms/lsc.cpp | 162 +---------------------------------
>  src/ipa/rkisp1/algorithms/lsc.h   |   1 -
>  4 files changed, 218 insertions(+), 170 deletions(-)
> 
> diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
> index a845d8b1783b..5c4ffc98797e 100644
> --- a/src/ipa/libipa/lsc_polynomial.cpp
> +++ b/src/ipa/libipa/lsc_polynomial.cpp
> @@ -2,11 +2,14 @@
>  /*
>   * Copyright (C) 2024, Ideas On Board
>   *
> - * Polynomial class to represent lens shading correction
> + * Polynomial based lens shading correction
>   */
>  
>  #include "lsc_polynomial.h"
>  
> +#include <assert.h>
> +#include <cmath>
> +
>  #include <libcamera/base/log.h>
>  
>  /**
> @@ -109,6 +112,180 @@ void Polynomial::setReferenceImageSize(const Size &size)
>  
>  } /* namespace lsc */
>  
> +/**
> + * \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

'using a Polynomial'

> + * map.
> + *
> + * \sa LscImplementation
> + */
> +
> +/**
> + * \fn LscPolynomial::LscPolynomial
> + * \param[in] sensorSize The physical sensor size
> + *
> + * Construct an LscPolynomial
> + */
> +
> +/**
> + * \brief Parse polynomial lsc data
> + * \param[in] sets The tuning file content
> + *
> + * Parse the lsc data in polyomial form from the \a sets tuning data.
> + *
> + * \return 0 on success or a negative error number otherwise
> + */
> +int LscPolynomial::parseLscData(const ValueNode &sets)
> +{
> +       for (const auto &set : sets.asList()) {
> +               std::optional<lsc::Polynomial> pr, pgr, pgb, pb;
> +               uint32_t ct = set["ct"].get<uint32_t>(0);
> +
> +               if (lscData_.count(ct)) {
> +                       LOG(LscPolynomial, Error)
> +                               << "Multiple sets found for "
> +                               << "color temperature " << ct;
> +                       return -EINVAL;
> +               }
> +
> +               pr = set["r"].get<lsc::Polynomial>();
> +               pgr = set["gr"].get<lsc::Polynomial>();
> +               pgb = set["gb"].get<lsc::Polynomial>();
> +               pb = set["b"].get<lsc::Polynomial>();
> +
> +               if (!(pr || pgr || pgb || pb)) {
> +                       LOG(LscPolynomial, 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(std::piecewise_construct,
> +                                std::forward_as_tuple(ct),
> +                                std::forward_as_tuple(PolynomialComponents{ *pr, *pgr, *pgb, *pb }));
> +       }
> +
> +       if (lscData_.empty()) {
> +               LOG(LscPolynomial, Error) << "Failed to load any sets";
> +               return -EINVAL;
> +       }
> +
> +       return 0;
> +}
> +
> +/**
> + * \brief Re-sample the lsc components for \a cropRectangle
> + * \param[in] cropRectangle The sensor analogue crop rectangle
> + * \param[in] xSizes List of horizontal positions of the lsc grid nodes
> + * \param[in] ySizes List of vertical positions of the lsc grid nodes
> + *
> + * Lsc tables have to be re-sampled every time a new sensor configuration is
> + * used, as each streaming session might use a different sensor crop rectangle.
> + *
> + * Polynomial Lsc tables can be re-sampled for a given sensor frame resolution
> + * using a list of horizontal and vertical nodes that define the lsc grid on
> + * which the polynomial is re-sampled on.
> + *
> + * \a cropRectangle represents the size of the frame on which the Lsc tables
> + * have to be re-sampled on.
> + *
> + * \a xSizes and \a ySizes 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.
> + */
> +lsc::ComponentsMap
> +LscPolynomial::sampleForCrop(const Rectangle &cropRectangle,
> +                            Span<const double> xSizes,
> +                            Span<const double> ySizes)
> +{
> +       std::vector<double> xPos = sizesListToPositions(xSizes);
> +       std::vector<double> yPos = sizesListToPositions(ySizes);
> +
> +       lsc::ComponentsMap components;
> +
> +       for (const auto &[k, p] : lscData_) {
> +               components[k] = {
> +                       samplePolynomial(p.pr, xPos, yPos, cropRectangle),
> +                       samplePolynomial(p.pgr, xPos, yPos, cropRectangle),
> +                       samplePolynomial(p.pgb, xPos, yPos, cropRectangle),
> +                       samplePolynomial(p.pb, xPos, yPos, cropRectangle)
> +               };
> +       }
> +
> +       return components;
> +}
> +
> +std::vector<uint16_t>
> +LscPolynomial::samplePolynomial(const lsc::Polynomial &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);

I see 'hardware' specifics here, but I assume they'll move out in the
refactoring steps next.

> +               }
> +       }
> +       return samples;
> +}
> +
> +/*
> + * The rkisp1 LSC grid spacing is defined by the cell sizes on the top-left

Same here, but I'm going to accept that these will become generics in
the series:


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

> + * 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>
> +LscPolynomial::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;
> +}
> +
>  } /* namespace ipa */
>  
>  #ifndef __DOXYGEN__
> diff --git a/src/ipa/libipa/lsc_polynomial.h b/src/ipa/libipa/lsc_polynomial.h
> index 2caf46d2d759..17c2c34499f0 100644
> --- a/src/ipa/libipa/lsc_polynomial.h
> +++ b/src/ipa/libipa/lsc_polynomial.h
> @@ -2,25 +2,23 @@
>  /*
>   * Copyright (C) 2024, 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 <map>
> +#include <vector>
>  
> -#include <libcamera/base/log.h>
>  #include <libcamera/base/span.h>
>  
>  #include <libcamera/geometry.h>
>  
>  #include "libcamera/internal/value_node.h"
>  
> -namespace libcamera {
> +#include "lsc_base.h"
>  
> -LOG_DECLARE_CATEGORY(LscPolynomial)
> +namespace libcamera {
>  
>  namespace ipa {
>  
> @@ -52,6 +50,40 @@ private:
>  
>  } /* namespace lsc */
>  
> +class LscPolynomial : public LscImplementation
> +{
> +private:
> +       struct PolynomialComponents {
> +               lsc::Polynomial pr;
> +               lsc::Polynomial pgr;
> +               lsc::Polynomial pgb;
> +               lsc::Polynomial pb;
> +       };
> +       using PolynomialComponentsMap = std::map<unsigned int, PolynomialComponents>;
> +
> +public:
> +       LscPolynomial(const Size &sensorSize)
> +               : sensorSize_(sensorSize)
> +       {
> +       }
> +
> +       int parseLscData(const ValueNode &sets) override;
> +
> +       lsc::ComponentsMap
> +       sampleForCrop(const Rectangle &cropRectangle,
> +                     Span<const double> xSizes,
> +                     Span<const double> ySizes) override;
> +
> +private:
> +       std::vector<double> sizesListToPositions(Span<const double> sizes);
> +       std::vector<uint16_t> samplePolynomial(const lsc::Polynomial &poly,
> +                                              Span<const double> xPositions,
> +                                              Span<const double> yPositions,
> +                                              const Rectangle &cropRectangle);
> +       PolynomialComponentsMap lscData_;
> +       Size sensorSize_;
> +};
> +
>  } /* namespace ipa */
>  
>  } /* namespace libcamera */
> diff --git a/src/ipa/rkisp1/algorithms/lsc.cpp b/src/ipa/rkisp1/algorithms/lsc.cpp
> index 910b2ced292e..a4ea879aaa83 100644
> --- a/src/ipa/rkisp1/algorithms/lsc.cpp
> +++ b/src/ipa/rkisp1/algorithms/lsc.cpp
> @@ -33,166 +33,6 @@ namespace {
>  
>  constexpr int kColourTemperatureQuantization = 10;
>  
> -class LscPolynomialImpl : public LscImplementation
> -{
> -private:
> -       struct PolynomialComponents {
> -               lsc::Polynomial pr;
> -               lsc::Polynomial pgr;
> -               lsc::Polynomial pgb;
> -               lsc::Polynomial pb;
> -       };
> -       using PolynomialComponentsMap = std::map<unsigned int, PolynomialComponents>;
> -
> -public:
> -       LscPolynomialImpl(const Size &sensorSize)
> -               : sensorSize_(sensorSize)
> -       {
> -       }
> -
> -       int parseLscData(const ValueNode &sets) override;
> -
> -       lsc::ComponentsMap
> -       sampleForCrop(const Rectangle &cropRectangle,
> -                     Span<const double> xSizes,
> -                     Span<const double> ySizes) override;
> -
> -private:
> -       std::vector<double> sizesListToPositions(Span<const double> sizes);
> -       std::vector<uint16_t> samplePolynomial(const lsc::Polynomial &poly,
> -                                              Span<const double> xPositions,
> -                                              Span<const double> yPositions,
> -                                              const Rectangle &cropRectangle);
> -       PolynomialComponentsMap lscData_;
> -       Size sensorSize_;
> -};
> -
> -int LscPolynomialImpl::parseLscData(const ValueNode &sets)
> -{
> -       for (const auto &set : sets.asList()) {
> -               std::optional<lsc::Polynomial> pr, pgr, pgb, pb;
> -               uint32_t ct = set["ct"].get<uint32_t>(0);
> -
> -               if (lscData_.count(ct)) {
> -                       LOG(RkISP1Lsc, Error)
> -                               << "Multiple sets found for "
> -                               << "color temperature " << ct;
> -                       return -EINVAL;
> -               }
> -
> -               pr = set["r"].get<lsc::Polynomial>();
> -               pgr = set["gr"].get<lsc::Polynomial>();
> -               pgb = set["gb"].get<lsc::Polynomial>();
> -               pb = set["b"].get<lsc::Polynomial>();
> -
> -               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(std::piecewise_construct,
> -                                std::forward_as_tuple(ct),
> -                                std::forward_as_tuple(PolynomialComponents{ *pr, *pgr, *pgb, *pb }));
> -       }
> -
> -       if (lscData_.empty()) {
> -               LOG(RkISP1Lsc, Error) << "Failed to load any sets";
> -               return -EINVAL;
> -       }
> -
> -       return 0;
> -}
> -
> -lsc::ComponentsMap
> -LscPolynomialImpl::sampleForCrop(const Rectangle &cropRectangle,
> -                                Span<const double> xSizes,
> -                                Span<const double> ySizes)
> -{
> -       std::vector<double> xPos = sizesListToPositions(xSizes);
> -       std::vector<double> yPos = sizesListToPositions(ySizes);
> -
> -       lsc::ComponentsMap components;
> -
> -       for (const auto &[k, p] : lscData_) {
> -               components[k] = {
> -                       samplePolynomial(p.pr, xPos, yPos, cropRectangle),
> -                       samplePolynomial(p.pgr, xPos, yPos, cropRectangle),
> -                       samplePolynomial(p.pgb, xPos, yPos, cropRectangle),
> -                       samplePolynomial(p.pb, xPos, yPos, cropRectangle)
> -               };
> -       }
> -
> -       return components;
> -}
> -
> -std::vector<uint16_t>
> -LscPolynomialImpl::samplePolynomial(const lsc::Polynomial &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>
> -LscPolynomialImpl::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 LscTableImpl : public LscImplementation
>  {
>  public:
> @@ -358,7 +198,7 @@ int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
>                  * \todo: Most likely the reference frame should be native_size.
>                  * Let's wait how the internal discussions progress.
>                  */
> -               algo_ = std::make_unique<LscPolynomialImpl>(context.sensorInfo.activeAreaSize);
> +               algo_ = std::make_unique<LscPolynomial>(context.sensorInfo.activeAreaSize);
>                 ret = algo_->parseLscData(sets);
>         } else {
>                 LOG(RkISP1Lsc, Error) << "Unsupported LSC data type '"
> diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
> index b21c91c0d81f..2a428293cb2e 100644
> --- a/src/ipa/rkisp1/algorithms/lsc.h
> +++ b/src/ipa/rkisp1/algorithms/lsc.h
> @@ -7,7 +7,6 @@
>  
>  #pragma once
>  
> -#include <map>
>  #include <memory>
>  
>  #include "libipa/interpolator.h"
> 
> -- 
> 2.54.0
>

Patch
diff mbox series

diff --git a/src/ipa/libipa/lsc_polynomial.cpp b/src/ipa/libipa/lsc_polynomial.cpp
index a845d8b1783b..5c4ffc98797e 100644
--- a/src/ipa/libipa/lsc_polynomial.cpp
+++ b/src/ipa/libipa/lsc_polynomial.cpp
@@ -2,11 +2,14 @@ 
 /*
  * Copyright (C) 2024, Ideas On Board
  *
- * Polynomial class to represent lens shading correction
+ * Polynomial based lens shading correction
  */
 
 #include "lsc_polynomial.h"
 
+#include <assert.h>
+#include <cmath>
+
 #include <libcamera/base/log.h>
 
 /**
@@ -109,6 +112,180 @@  void Polynomial::setReferenceImageSize(const Size &size)
 
 } /* namespace lsc */
 
+/**
+ * \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
+ */
+
+/**
+ * \fn LscPolynomial::LscPolynomial
+ * \param[in] sensorSize The physical sensor size
+ *
+ * Construct an LscPolynomial
+ */
+
+/**
+ * \brief Parse polynomial lsc data
+ * \param[in] sets The tuning file content
+ *
+ * Parse the lsc data in polyomial form from the \a sets tuning data.
+ *
+ * \return 0 on success or a negative error number otherwise
+ */
+int LscPolynomial::parseLscData(const ValueNode &sets)
+{
+	for (const auto &set : sets.asList()) {
+		std::optional<lsc::Polynomial> pr, pgr, pgb, pb;
+		uint32_t ct = set["ct"].get<uint32_t>(0);
+
+		if (lscData_.count(ct)) {
+			LOG(LscPolynomial, Error)
+				<< "Multiple sets found for "
+				<< "color temperature " << ct;
+			return -EINVAL;
+		}
+
+		pr = set["r"].get<lsc::Polynomial>();
+		pgr = set["gr"].get<lsc::Polynomial>();
+		pgb = set["gb"].get<lsc::Polynomial>();
+		pb = set["b"].get<lsc::Polynomial>();
+
+		if (!(pr || pgr || pgb || pb)) {
+			LOG(LscPolynomial, 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(std::piecewise_construct,
+				 std::forward_as_tuple(ct),
+				 std::forward_as_tuple(PolynomialComponents{ *pr, *pgr, *pgb, *pb }));
+	}
+
+	if (lscData_.empty()) {
+		LOG(LscPolynomial, Error) << "Failed to load any sets";
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+/**
+ * \brief Re-sample the lsc components for \a cropRectangle
+ * \param[in] cropRectangle The sensor analogue crop rectangle
+ * \param[in] xSizes List of horizontal positions of the lsc grid nodes
+ * \param[in] ySizes List of vertical positions of the lsc grid nodes
+ *
+ * Lsc tables have to be re-sampled every time a new sensor configuration is
+ * used, as each streaming session might use a different sensor crop rectangle.
+ *
+ * Polynomial Lsc tables can be re-sampled for a given sensor frame resolution
+ * using a list of horizontal and vertical nodes that define the lsc grid on
+ * which the polynomial is re-sampled on.
+ *
+ * \a cropRectangle represents the size of the frame on which the Lsc tables
+ * have to be re-sampled on.
+ *
+ * \a xSizes and \a ySizes 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.
+ */
+lsc::ComponentsMap
+LscPolynomial::sampleForCrop(const Rectangle &cropRectangle,
+			     Span<const double> xSizes,
+			     Span<const double> ySizes)
+{
+	std::vector<double> xPos = sizesListToPositions(xSizes);
+	std::vector<double> yPos = sizesListToPositions(ySizes);
+
+	lsc::ComponentsMap components;
+
+	for (const auto &[k, p] : lscData_) {
+		components[k] = {
+			samplePolynomial(p.pr, xPos, yPos, cropRectangle),
+			samplePolynomial(p.pgr, xPos, yPos, cropRectangle),
+			samplePolynomial(p.pgb, xPos, yPos, cropRectangle),
+			samplePolynomial(p.pb, xPos, yPos, cropRectangle)
+		};
+	}
+
+	return components;
+}
+
+std::vector<uint16_t>
+LscPolynomial::samplePolynomial(const lsc::Polynomial &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>
+LscPolynomial::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;
+}
+
 } /* namespace ipa */
 
 #ifndef __DOXYGEN__
diff --git a/src/ipa/libipa/lsc_polynomial.h b/src/ipa/libipa/lsc_polynomial.h
index 2caf46d2d759..17c2c34499f0 100644
--- a/src/ipa/libipa/lsc_polynomial.h
+++ b/src/ipa/libipa/lsc_polynomial.h
@@ -2,25 +2,23 @@ 
 /*
  * Copyright (C) 2024, 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 <map>
+#include <vector>
 
-#include <libcamera/base/log.h>
 #include <libcamera/base/span.h>
 
 #include <libcamera/geometry.h>
 
 #include "libcamera/internal/value_node.h"
 
-namespace libcamera {
+#include "lsc_base.h"
 
-LOG_DECLARE_CATEGORY(LscPolynomial)
+namespace libcamera {
 
 namespace ipa {
 
@@ -52,6 +50,40 @@  private:
 
 } /* namespace lsc */
 
+class LscPolynomial : public LscImplementation
+{
+private:
+	struct PolynomialComponents {
+		lsc::Polynomial pr;
+		lsc::Polynomial pgr;
+		lsc::Polynomial pgb;
+		lsc::Polynomial pb;
+	};
+	using PolynomialComponentsMap = std::map<unsigned int, PolynomialComponents>;
+
+public:
+	LscPolynomial(const Size &sensorSize)
+		: sensorSize_(sensorSize)
+	{
+	}
+
+	int parseLscData(const ValueNode &sets) override;
+
+	lsc::ComponentsMap
+	sampleForCrop(const Rectangle &cropRectangle,
+		      Span<const double> xSizes,
+		      Span<const double> ySizes) override;
+
+private:
+	std::vector<double> sizesListToPositions(Span<const double> sizes);
+	std::vector<uint16_t> samplePolynomial(const lsc::Polynomial &poly,
+					       Span<const double> xPositions,
+					       Span<const double> yPositions,
+					       const Rectangle &cropRectangle);
+	PolynomialComponentsMap lscData_;
+	Size sensorSize_;
+};
+
 } /* namespace ipa */
 
 } /* namespace libcamera */
diff --git a/src/ipa/rkisp1/algorithms/lsc.cpp b/src/ipa/rkisp1/algorithms/lsc.cpp
index 910b2ced292e..a4ea879aaa83 100644
--- a/src/ipa/rkisp1/algorithms/lsc.cpp
+++ b/src/ipa/rkisp1/algorithms/lsc.cpp
@@ -33,166 +33,6 @@  namespace {
 
 constexpr int kColourTemperatureQuantization = 10;
 
-class LscPolynomialImpl : public LscImplementation
-{
-private:
-	struct PolynomialComponents {
-		lsc::Polynomial pr;
-		lsc::Polynomial pgr;
-		lsc::Polynomial pgb;
-		lsc::Polynomial pb;
-	};
-	using PolynomialComponentsMap = std::map<unsigned int, PolynomialComponents>;
-
-public:
-	LscPolynomialImpl(const Size &sensorSize)
-		: sensorSize_(sensorSize)
-	{
-	}
-
-	int parseLscData(const ValueNode &sets) override;
-
-	lsc::ComponentsMap
-	sampleForCrop(const Rectangle &cropRectangle,
-		      Span<const double> xSizes,
-		      Span<const double> ySizes) override;
-
-private:
-	std::vector<double> sizesListToPositions(Span<const double> sizes);
-	std::vector<uint16_t> samplePolynomial(const lsc::Polynomial &poly,
-					       Span<const double> xPositions,
-					       Span<const double> yPositions,
-					       const Rectangle &cropRectangle);
-	PolynomialComponentsMap lscData_;
-	Size sensorSize_;
-};
-
-int LscPolynomialImpl::parseLscData(const ValueNode &sets)
-{
-	for (const auto &set : sets.asList()) {
-		std::optional<lsc::Polynomial> pr, pgr, pgb, pb;
-		uint32_t ct = set["ct"].get<uint32_t>(0);
-
-		if (lscData_.count(ct)) {
-			LOG(RkISP1Lsc, Error)
-				<< "Multiple sets found for "
-				<< "color temperature " << ct;
-			return -EINVAL;
-		}
-
-		pr = set["r"].get<lsc::Polynomial>();
-		pgr = set["gr"].get<lsc::Polynomial>();
-		pgb = set["gb"].get<lsc::Polynomial>();
-		pb = set["b"].get<lsc::Polynomial>();
-
-		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(std::piecewise_construct,
-				 std::forward_as_tuple(ct),
-				 std::forward_as_tuple(PolynomialComponents{ *pr, *pgr, *pgb, *pb }));
-	}
-
-	if (lscData_.empty()) {
-		LOG(RkISP1Lsc, Error) << "Failed to load any sets";
-		return -EINVAL;
-	}
-
-	return 0;
-}
-
-lsc::ComponentsMap
-LscPolynomialImpl::sampleForCrop(const Rectangle &cropRectangle,
-				 Span<const double> xSizes,
-				 Span<const double> ySizes)
-{
-	std::vector<double> xPos = sizesListToPositions(xSizes);
-	std::vector<double> yPos = sizesListToPositions(ySizes);
-
-	lsc::ComponentsMap components;
-
-	for (const auto &[k, p] : lscData_) {
-		components[k] = {
-			samplePolynomial(p.pr, xPos, yPos, cropRectangle),
-			samplePolynomial(p.pgr, xPos, yPos, cropRectangle),
-			samplePolynomial(p.pgb, xPos, yPos, cropRectangle),
-			samplePolynomial(p.pb, xPos, yPos, cropRectangle)
-		};
-	}
-
-	return components;
-}
-
-std::vector<uint16_t>
-LscPolynomialImpl::samplePolynomial(const lsc::Polynomial &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>
-LscPolynomialImpl::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 LscTableImpl : public LscImplementation
 {
 public:
@@ -358,7 +198,7 @@  int LensShadingCorrection::init([[maybe_unused]] IPAContext &context,
 		 * \todo: Most likely the reference frame should be native_size.
 		 * Let's wait how the internal discussions progress.
 		 */
-		algo_ = std::make_unique<LscPolynomialImpl>(context.sensorInfo.activeAreaSize);
+		algo_ = std::make_unique<LscPolynomial>(context.sensorInfo.activeAreaSize);
 		ret = algo_->parseLscData(sets);
 	} else {
 		LOG(RkISP1Lsc, Error) << "Unsupported LSC data type '"
diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
index b21c91c0d81f..2a428293cb2e 100644
--- a/src/ipa/rkisp1/algorithms/lsc.h
+++ b/src/ipa/rkisp1/algorithms/lsc.h
@@ -7,7 +7,6 @@ 
 
 #pragma once
 
-#include <map>
 #include <memory>
 
 #include "libipa/interpolator.h"