diff --git a/src/ipa/libipa/lsc.cpp b/src/ipa/libipa/lsc.cpp
index 8c5c28077cce..8f881edef94c 100644
--- a/src/ipa/libipa/lsc.cpp
+++ b/src/ipa/libipa/lsc.cpp
@@ -48,6 +48,29 @@ namespace lsc {
 
 } /* namespace lsc */
 
+/**
+ * \class LscAlgorithmBase
+ * \brief Base class for LscAlgorithm
+ *
+ * Base class for LscAlgorithm for non-templated functions implementation
+ */
+
+/**
+ * \var LscAlgorithmBase::impl_
+ * \brief The LSC algorithm implementation
+ *
+ * There are two classes derived from LscImplementation, the LscTable and
+ * LscPolynomial ones. Which one to instantiate is decided by parsing the tuning
+ * file.
+ */
+
+/**
+ * \var LscAlgorithmBase::polynomial_
+ * \brief Boolean flag for polynomial LSC
+ *
+ * Set to true if polynomial LSC is in use.
+ */
+
 /**
  * \class LscAlgorithm
  * \brief libIPA LSC algorithm implementation
@@ -199,6 +222,16 @@ namespace lsc {
  * can retrieve them using LscAlgorithm::getComponents().
  */
 
+/**
+ * \typedef LscAlgorithm::Components
+ * \brief Map a colour component to a list of gains
+ */
+
+/**
+ * \typedef LscAlgorithm::ComponentsMap
+ * \brief Map a colour temperature to an LSC componenet
+ */
+
 /**
  * \param[in] tuningData The tuning data
  * \param[in] controls The IPA list of supported controls
@@ -209,8 +242,8 @@ namespace lsc {
  *
  * \return 0 on success, a negative error code otherwise
  */
-int LscAlgorithm::init(const ValueNode &tuningData, ControlInfoMap::Map &controls,
-		       const LscDescriptor &descriptor)
+int LscAlgorithmBase::init(const ValueNode &tuningData, ControlInfoMap::Map &controls,
+			   const LscDescriptor &descriptor)
 {
 	polynomial_ = false;
 
@@ -245,6 +278,7 @@ int LscAlgorithm::init(const ValueNode &tuningData, ControlInfoMap::Map &control
 }
 
 /**
+ * \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
@@ -265,28 +299,6 @@ int LscAlgorithm::init(const ValueNode &tuningData, ControlInfoMap::Map &control
  *
  * \return 0 on success, a negative error code otherwise
  */
-int LscAlgorithm::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 lscData =
-		impl_->sampleForCrop(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;
-}
 
 /**
  * \brief Queue a request to the lsc algorithm
@@ -297,9 +309,9 @@ int LscAlgorithm::configure(lsc::ActiveState &state, const Rectangle &analogCrop
  * Queue a new list of \a controls to the lsc algorithm.
  * The only supported control is controls::LensShadingCorrectionEnable.
  */
-void LscAlgorithm::queueRequest(lsc::ActiveState &state,
-				lsc::FrameContext &context,
-				const ControlList &controls)
+void LscAlgorithmBase::queueRequest(lsc::ActiveState &state,
+				    lsc::FrameContext &context,
+				    const ControlList &controls)
 {
 	const auto &lscEnable = controls.get(controls::LensShadingCorrectionEnable);
 	if (lscEnable && *lscEnable != state.enabled) {
@@ -322,25 +334,22 @@ void LscAlgorithm::queueRequest(lsc::ActiveState &state,
  * Populates the list of \a metadata with controls handled by the LscAlgorithm
  * class. The only supported metadata is controls::LensShadingCorrectionEnable.
  */
-void LscAlgorithm::process(lsc::FrameContext &context, ControlList &metadata)
+void LscAlgorithmBase::process(lsc::FrameContext &context, ControlList &metadata)
 {
 	metadata.set(controls::LensShadingCorrectionEnable, context.enabled);
 }
 
 /**
- * \fn LscAlgorithm::interpolateComponents
- * \brief Interpolate the LSC tables for a given colour temperature
- * \param[in] ct The colour temperature
+ * \fn LscAlgorithm::getInterpolator
+ * \brief Retrieve the LSC tables interpolator
  *
  * LSC tables are generated using different colour temperatures during the
- * tuning phase.
- *
- * This function returns the interpolated LSC data for a given \a ct
- * colour temperature.
+ * tuning phase. This function returns the interpolator for the LSC data.
  *
- * IPA algorithm can use this function to obtain a list of per-colour-component
- * gains to program their LSC engines with every time a significant enough
- * change in colour temperature is detected.
+ * IPA algorithm can use the interpolator to obtain a list of
+ * per-colour-component gains to program their LSC engines with every time a
+ * significant enough change in colour temperature is detected by calling the
+ * Interpolator::getInterpolated() function.
  *
  * Calling this function is only valid after LscAlgorithm::configure() has been
  * called. An empty components list is returned otherwise.
@@ -350,8 +359,7 @@ void LscAlgorithm::process(lsc::FrameContext &context, ControlList &metadata)
 
 /**
  * \fn LscAlgorithm::getComponents
- *
- * Return the map of LSC data per-colour-temperature.
+ * \brief 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.
diff --git a/src/ipa/libipa/lsc.h b/src/ipa/libipa/lsc.h
index 9fe8ad67ea33..15c8a9cb7b9c 100644
--- a/src/ipa/libipa/lsc.h
+++ b/src/ipa/libipa/lsc.h
@@ -35,35 +35,101 @@ struct FrameContext {
 
 } /* namespace lsc */
 
-class LscAlgorithm
+class LscAlgorithmBase
 {
 public:
 	int init(const ValueNode &tuningData, ControlInfoMap::Map &controls,
 		 const LscDescriptor &descriptor);
 
-	int configure(lsc::ActiveState &state, const Rectangle &analogCrop,
-		      const std::vector<double> &xPos,
-		      const std::vector<double> &yPos);
-
 	void queueRequest(lsc::ActiveState &state, lsc::FrameContext &context,
 			  const ControlList &controls);
 	void process(lsc::FrameContext &context, ControlList &metadata);
 
-	const lsc::Components interpolateComponents(unsigned int ct)
+protected:
+	LscAlgorithmBase() = default;
+
+	std::unique_ptr<LscImplementation> impl_;
+	bool polynomial_;
+};
+
+template<typename U>
+class LscAlgorithm : public LscAlgorithmBase
+{
+private:
+	using T = typename U::QuantizedType;
+
+	template<typename V>
+	class _Components : public std::map<std::string, std::vector<V>>
+	{
+	};
+
+	template<typename V>
+	class _ComponentsMap : public std::map<unsigned int, _Components<V>>
 	{
-		return sets_.getInterpolated(ct);
+	};
+
+public:
+	using Components = _Components<T>;
+	using ComponentsMap = _ComponentsMap<T>;
+
+	LscAlgorithm() = default;
+
+	int configure(lsc::ActiveState &state, const Rectangle &analogCrop,
+		      const std::vector<double> &xPos,
+		      const std::vector<double> &yPos)
+	{
+		lsc::ComponentsMap data =
+			impl_->sampleForCrop(analogCrop, xPos, yPos);
+
+		ComponentsMap lscData;
+		for (const auto &[t, c] : data) {
+			Components comp;
+
+			for (const auto &[k, gains] : c) {
+				std::vector<T> quantizedGains;
+				quantizedGains.reserve(gains.size());
+
+				for (const float &gain : gains) {
+					if (polynomial_)
+						quantizedGains.push_back(U(gain).quantized());
+					else
+						quantizedGains.push_back(gain);
+				}
+
+				comp[k] = std::move(quantizedGains);
+			}
+
+			lscData[t] = comp;
+		}
+
+		/*
+		 * 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;
+	}
+
+	Interpolator<Components> &getInterpolator()
+	{
+		return sets_;
 	}
 
-	const lsc::ComponentsMap getComponents()
+	const ComponentsMap &getComponents() const
 	{
 		return lscData_;
 	}
 
 private:
-	std::unique_ptr<LscImplementation> impl_;
-	Interpolator<lsc::Components> sets_;
-	lsc::ComponentsMap lscData_;
-	bool polynomial_;
+	ComponentsMap lscData_;
+	Interpolator<Components> sets_;
 };
 
 } /* namespace ipa */
diff --git a/src/ipa/libipa/lsc_base.cpp b/src/ipa/libipa/lsc_base.cpp
index cf2c3bfc382f..a98358daf4ec 100644
--- a/src/ipa/libipa/lsc_base.cpp
+++ b/src/ipa/libipa/lsc_base.cpp
@@ -46,19 +46,6 @@ namespace lsc {
 
 } /* namespace lsc */
 
-#ifndef __DOXYGEN__
-template<>
-void Interpolator<lsc::Components>::
-	interpolate(const lsc::Components &a,
-		    const lsc::Components &b,
-		    lsc::Components &dest,
-		    double lambda)
-{
-	for (auto const &[k, v] : a)
-		interpolateVector(v, b.at(k), dest[k], lambda);
-}
-#endif
-
 /**
  * \struct LscDescriptor
  * \brief Describe the ISP LSC engine
diff --git a/src/ipa/libipa/lsc_base.h b/src/ipa/libipa/lsc_base.h
index 386cac55be34..8053de7d2edb 100644
--- a/src/ipa/libipa/lsc_base.h
+++ b/src/ipa/libipa/lsc_base.h
@@ -31,25 +31,6 @@ using ComponentsMap = std::map<unsigned int, Components>;
 
 } /* 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>::
-	interpolate(const lsc::Components &a,
-		    const lsc::Components &b,
-		    lsc::Components &dest,
-		    double lambda);
-#endif /* __DOXYGEN__ */
-
 struct LscDescriptor {
 	std::vector<std::string> keys;
 	unsigned int numHSamples;
diff --git a/src/ipa/rkisp1/algorithms/lsc.cpp b/src/ipa/rkisp1/algorithms/lsc.cpp
index dba1a01118ee..157403691ffc 100644
--- a/src/ipa/rkisp1/algorithms/lsc.cpp
+++ b/src/ipa/rkisp1/algorithms/lsc.cpp
@@ -175,34 +175,16 @@ void LensShadingCorrection::setParameters(rkisp1_cif_isp_lsc_config &config)
 }
 
 void LensShadingCorrection::copyTable(rkisp1_cif_isp_lsc_config &config,
-				      const lsc::Components &set)
+				      const RkISP1Components &set)
 {
-	/*
-	 * The hardware uses 2.10 fixed point format and limits the legal values
-	 * to [1..3.999]. Scale and clamp the sampled values accordingly.
-	 */
-	std::vector<uint16_t> regs;
-	regs.reserve(RKISP1_CIF_ISP_LSC_SAMPLES_MAX *
-		     RKISP1_CIF_ISP_LSC_SAMPLES_MAX);
-
-	for (const float &f : set.at("r"))
-		regs.emplace_back(std::clamp(static_cast<int>(f * 1024), 1024, 4095));
-	std::copy(regs.begin(), regs.end(), &config.r_data_tbl[0][0]);
-
-	regs = {};
-	for (const float &f : set.at("gr"))
-		regs.emplace_back(std::clamp(static_cast<int>(f * 1024), 1024, 4095));
-	std::copy(regs.begin(), regs.end(), &config.gr_data_tbl[0][0]);
-
-	regs = {};
-	for (const float &f : set.at("gb"))
-		regs.emplace_back(std::clamp(static_cast<int>(f * 1024), 1024, 4095));
-	std::copy(regs.begin(), regs.end(), &config.gb_data_tbl[0][0]);
-
-	regs = {};
-	for (const float &f : set.at("b"))
-		regs.emplace_back(std::clamp(static_cast<int>(f * 1024), 1024, 4095));
-	std::copy(regs.begin(), regs.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]);
 }
 
 /**
@@ -252,8 +234,8 @@ void LensShadingCorrection::prepare([[maybe_unused]] IPAContext &context,
 
 	setParameters(*config);
 
-	const lsc::Components &set = lscAlgo_.interpolateComponents(quantizedCt);
-	copyTable(*config, set);
+	auto &lscData = lscAlgo_.getInterpolator();
+	copyTable(*config, lscData.getInterpolated(quantizedCt));
 
 	lastAppliedCt_ = ct;
 	lastAppliedQuantizedCt_ = quantizedCt;
diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
index 63f0887f0838..55a6980014f2 100644
--- a/src/ipa/rkisp1/algorithms/lsc.h
+++ b/src/ipa/rkisp1/algorithms/lsc.h
@@ -12,6 +12,7 @@
 #include <linux/rkisp1-config.h>
 
 #include "libcamera/internal/value_node.h"
+#include "libipa/fixedpoint.h"
 
 #include "libipa/lsc.h"
 
@@ -19,7 +20,13 @@
 
 namespace libcamera {
 
-namespace ipa::rkisp1::algorithms {
+namespace ipa {
+
+namespace rkisp1::algorithms {
+
+using RkISP1LscAlgorithm = LscAlgorithm<UQ<2, 10>>;
+using RkISP1Components = RkISP1LscAlgorithm::Components;
+using RkISP1ComponentsMap = RkISP1LscAlgorithm::ComponentsMap;
 
 class LensShadingCorrection : public Algorithm
 {
@@ -39,9 +46,11 @@ public:
 		     IPAFrameContext &frameContext,
 		     const rkisp1_stat_buffer *stats,
 		     ControlList &metadata) override;
+
 private:
 	void setParameters(rkisp1_cif_isp_lsc_config &config);
-	void copyTable(rkisp1_cif_isp_lsc_config &config, const lsc::Components &set0);
+	void copyTable(rkisp1_cif_isp_lsc_config &config,
+		       const RkISP1Components &set);
 
 	std::vector<double> xSize_;
 	std::vector<double> ySize_;
@@ -55,8 +64,35 @@ private:
 	unsigned int lastAppliedCt_;
 	unsigned int lastAppliedQuantizedCt_;
 
-	LscAlgorithm lscAlgo_;
+	RkISP1LscAlgorithm lscAlgo_;
 };
 
-} /* namespace ipa::rkisp1::algorithms */
+} /* namespace rkisp1::algorithms */
+
+#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<rkisp1::algorithms::RkISP1Components>::
+	interpolate(const rkisp1::algorithms::RkISP1Components &a,
+		    const rkisp1::algorithms::RkISP1Components &b,
+		    rkisp1::algorithms::RkISP1Components &dest,
+		    double lambda)
+{
+	for (auto const &[k, v] : a)
+		interpolateVector(v, b.at(k), dest[k], lambda);
+}
+#endif /* __DOXYGEN__ */
+
+} /* namespace ipa */
+
 } /* namespace libcamera */
