diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
index 37fff49a2..bef6820cb 100644
--- a/include/libcamera/internal/software_isp/debayer_params.h
+++ b/include/libcamera/internal/software_isp/debayer_params.h
@@ -37,8 +37,12 @@ struct DebayerParams {
 	enum LscType : uint32_t {
 		LscNone,
 		LscTable,
+		LscPolynomial,
 	};
 	LscLookupTable lscLut{};
+
+	static constexpr unsigned int kNLscCoefficients = 3;
+	std::array<RGB<float>, kNLscCoefficients> lscCoefficients;
 };
 
 } /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/lsc.cpp b/src/ipa/simple/algorithms/lsc.cpp
index ae5179cfc..0470b5e76 100644
--- a/src/ipa/simple/algorithms/lsc.cpp
+++ b/src/ipa/simple/algorithms/lsc.cpp
@@ -18,23 +18,27 @@ LOG_DEFINE_CATEGORY(IPASoftLsc)
 int Lsc::init(IPAContext &context, const ValueNode &tuningData)
 {
 	std::string type = tuningData["type"].get<std::string>("table");
+	int retR, retG, retB;
 
 	if (type == "table") {
-		int retR = lscR_.readYaml(tuningData["sets"], "ct", "r");
-		int retG = lscG_.readYaml(tuningData["sets"], "ct", "g");
-		int retB = lscB_.readYaml(tuningData["sets"], "ct", "b");
-
-		if (retR < 0 || retG < 0 || retB < 0) {
-			LOG(IPASoftLsc, Error)
-				<< "Failed to parse 'lsc' parameter from tuning file.";
-			return -EINVAL;
-		}
-
+		retR = lscR_.readYaml(tuningData["sets"], "ct", "r");
+		retG = lscG_.readYaml(tuningData["sets"], "ct", "g");
+		retB = lscB_.readYaml(tuningData["sets"], "ct", "b");
 		type_ = DebayerParams::LscTable;
+	} else if (type == "polynomial") {
+		retR = lscCoefR_.readYaml(tuningData["sets"], "ct", "r");
+		retG = lscCoefG_.readYaml(tuningData["sets"], "ct", "g");
+		retB = lscCoefB_.readYaml(tuningData["sets"], "ct", "b");
+		type_ = DebayerParams::LscPolynomial;
 	} else {
 		LOG(IPASoftLsc, Error) << "LSC: type " << type << " not supported";
 		return -EINVAL;
 	}
+	if (retR < 0 || retG < 0 || retB < 0) {
+		LOG(IPASoftLsc, Error)
+			<< "Failed to parse 'lsc' parameter from tuning file.";
+		return -EINVAL;
+	}
 
 	context.lscType = type_;
 
@@ -53,18 +57,43 @@ void Lsc::prepare(IPAContext &context, [[maybe_unused]] const uint32_t frame,
 	unsigned int ct =
 		context.activeState.awb.temperatureK.value_or(kDefaultTemperature);
 
-	const LscMatrix matrixR = lscR_.getInterpolated(ct);
-	const LscMatrix matrixG = lscG_.getInterpolated(ct);
-	const LscMatrix matrixB = lscB_.getInterpolated(ct);
+	switch (type_) {
+	case DebayerParams::LscNone:
+		break;
+
+	case DebayerParams::LscTable: {
+		const LscMatrix matrixR = lscR_.getInterpolated(ct);
+		const LscMatrix matrixG = lscG_.getInterpolated(ct);
+		const LscMatrix matrixB = lscB_.getInterpolated(ct);
+
+		DebayerParams::LscLookupTable lut;
+		constexpr unsigned int gridSize = DebayerParams::kLscGridSize;
+		for (unsigned int i = 0, j = 0; i < gridSize * gridSize; i++) {
+			lut[j++] = matrixR.data()[i];
+			lut[j++] = matrixG.data()[i];
+			lut[j++] = matrixB.data()[i];
+		}
+		params->lscLut = lut;
 
-	DebayerParams::LscLookupTable lut;
-	constexpr unsigned int gridSize = DebayerParams::kLscGridSize;
-	for (unsigned int i = 0, j = 0; i < gridSize * gridSize; i++) {
-		lut[j++] = matrixR.data()[i];
-		lut[j++] = matrixG.data()[i];
-		lut[j++] = matrixB.data()[i];
+		break;
+	}
+
+	case DebayerParams::LscPolynomial: {
+		const Vector<float, DebayerParams::kNLscCoefficients> coefR =
+			lscCoefR_.getInterpolated(ct);
+		const Vector<float, DebayerParams::kNLscCoefficients> coefG =
+			lscCoefG_.getInterpolated(ct);
+		const Vector<float, DebayerParams::kNLscCoefficients> coefB =
+			lscCoefB_.getInterpolated(ct);
+
+		for (unsigned int i = 0; i < DebayerParams::kNLscCoefficients; i++) {
+			params->lscCoefficients[i].r() = coefR[i];
+			params->lscCoefficients[i].g() = coefG[i];
+			params->lscCoefficients[i].b() = coefB[i];
+		}
+		break;
+	}
 	}
-	params->lscLut = lut;
 }
 
 REGISTER_IPA_ALGORITHM(Lsc, "Lsc")
diff --git a/src/ipa/simple/algorithms/lsc.h b/src/ipa/simple/algorithms/lsc.h
index d7d7c9559..e1981dec7 100644
--- a/src/ipa/simple/algorithms/lsc.h
+++ b/src/ipa/simple/algorithms/lsc.h
@@ -6,6 +6,7 @@
 #pragma once
 
 #include "libcamera/internal/matrix.h"
+#include "libcamera/internal/vector.h"
 
 #include <libipa/interpolator.h>
 
@@ -35,6 +36,9 @@ private:
 	Interpolator<LscMatrix> lscR_;
 	Interpolator<LscMatrix> lscG_;
 	Interpolator<LscMatrix> lscB_;
+	Interpolator<Vector<float, DebayerParams::kNLscCoefficients>> lscCoefR_;
+	Interpolator<Vector<float, DebayerParams::kNLscCoefficients>> lscCoefG_;
+	Interpolator<Vector<float, DebayerParams::kNLscCoefficients>> lscCoefB_;
 };
 
 } /* namespace ipa::soft::algorithms */
diff --git a/src/libcamera/shaders/bayer_1x_packed.frag b/src/libcamera/shaders/bayer_1x_packed.frag
index dc2b582f8..bc45c1993 100644
--- a/src/libcamera/shaders/bayer_1x_packed.frag
+++ b/src/libcamera/shaders/bayer_1x_packed.frag
@@ -72,6 +72,11 @@ uniform float contrastExp;
 
 #if defined(APPLY_LSC_TABLE)
 uniform sampler2D lsc_tex;
+#elif defined(APPLY_LSC_POLYNOMIAL)
+uniform vec2            lscScale;
+uniform vec3            lsc0;
+uniform vec3            lsc1;
+uniform vec3            lsc2;
 #endif
 
 float apply_contrast(float value)
@@ -233,6 +238,10 @@ void main(void)
 
 #if defined(APPLY_LSC_TABLE)
 	rgb = rgb * texture2D(lsc_tex, textureOut).rgb;
+#elif defined(APPLY_LSC_POLYNOMIAL)
+	vec2 offCenter = (textureOut - vec2(0.5, 0.5)) * lscScale;
+	float dist2 = dot(offCenter, offCenter);
+	rgb = rgb * (lsc0 + lsc1 * dist2 + lsc2 * dist2 * dist2);
 #endif
 
 	/*
diff --git a/src/libcamera/shaders/bayer_unpacked.frag b/src/libcamera/shaders/bayer_unpacked.frag
index df324e6e1..5a1d82325 100644
--- a/src/libcamera/shaders/bayer_unpacked.frag
+++ b/src/libcamera/shaders/bayer_unpacked.frag
@@ -31,6 +31,11 @@ uniform float           contrastExp;
 
 #if defined(APPLY_LSC_TABLE)
 uniform sampler2D lsc_tex;
+#elif defined(APPLY_LSC_POLYNOMIAL)
+uniform vec2            lscScale;
+uniform vec3            lsc0;
+uniform vec3            lsc1;
+uniform vec3            lsc2;
 #endif
 
 float apply_contrast(float value)
@@ -136,6 +141,10 @@ void main(void) {
 
 #if defined(APPLY_LSC_TABLE)
     rgb = rgb * texture2D(lsc_tex, center.xy).rgb;
+#elif defined(APPLY_LSC_POLYNOMIAL)
+    vec2 offCenter = (center.xy - vec2(0.5, 0.5)) * lscScale;
+    float dist2 = dot(offCenter, offCenter);
+    rgb = rgb * (lsc0 + lsc1 * dist2 + lsc2 * dist2 * dist2);
 #endif
 
     /*
diff --git a/src/libcamera/software_isp/debayer.cpp b/src/libcamera/software_isp/debayer.cpp
index a77b3c9fc..ec0c618ba 100644
--- a/src/libcamera/software_isp/debayer.cpp
+++ b/src/libcamera/software_isp/debayer.cpp
@@ -63,6 +63,11 @@ namespace libcamera {
  * \brief Lens shading correction using a lookup table
  */
 
+/**
+ * \var DebayerParams::LscPolynomial
+ * \brief Lens shading correction using polynomial coefficients
+ */
+
 /**
  * \typedef DebayerParams::LscValueType
  * \brief Type of LSC grid values
@@ -86,6 +91,16 @@ namespace libcamera {
  * \brief Lens shading lookup table
  */
 
+/**
+ * \var DebayerParams::kNLscCoefficients
+ * \brief Number of the lens shading correction polynomial coefficients
+ */
+
+/**
+ * \var DebayerParams::lscCoefficients
+ * \brief Polynomial coefficients for lens shading correction, one per colour channel
+ */
+
 /**
  * \class Debayer
  * \brief Base debayering class
diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
index 03ec5c5da..560a87d66 100644
--- a/src/libcamera/software_isp/debayer_egl.cpp
+++ b/src/libcamera/software_isp/debayer_egl.cpp
@@ -116,6 +116,10 @@ int DebayerEGL::getShaderVariableLocations(void)
 	textureUniformProjMatrix_ = glGetUniformLocation(programId_, "proj_matrix");
 
 	textureUniformLsc_ = glGetUniformLocation(programId_, "lsc_tex");
+	lscScale_ = glGetUniformLocation(programId_, "lscScale");
+	lsc0_ = glGetUniformLocation(programId_, "lsc0");
+	lsc1_ = glGetUniformLocation(programId_, "lsc1");
+	lsc2_ = glGetUniformLocation(programId_, "lsc2");
 
 	LOG(Debayer, Debug) << "vertexIn " << attributeVertex_ << " textureIn " << attributeTexture_
 			    << " tex_y " << textureUniformBayerDataIn_
@@ -128,7 +132,11 @@ int DebayerEGL::getShaderVariableLocations(void)
 			    << " stride_factor " << textureUniformStrideFactor_
 			    << " tex_bayer_first_red " << textureUniformBayerFirstRed_
 			    << " proj_matrix " << textureUniformProjMatrix_
-			    << " lsc " << textureUniformLsc_;
+			    << " lscTexture " << textureUniformLsc_
+			    << " lscScale " << lscScale_
+			    << " lsc0 " << lsc0_
+			    << " lsc1 " << lsc1_
+			    << " lsc2 " << lsc2_;
 	return 0;
 }
 
@@ -153,6 +161,9 @@ int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm
 	case DebayerParams::LscTable:
 		egl_.pushEnv(shaderEnv, "#define APPLY_LSC_TABLE");
 		break;
+	case DebayerParams::LscPolynomial:
+		egl_.pushEnv(shaderEnv, "#define APPLY_LSC_POLYNOMIAL");
+		break;
 	}
 
 	/*
@@ -513,6 +524,21 @@ void DebayerEGL::setShaderVariableValues(const DebayerParams &params)
 				     params.lscLut.data(), GL_LINEAR);
 		glUniform1i(textureUniformLsc_, eglImageLscLookup_->texture_unit_uniform_id_);
 		break;
+	case DebayerParams::LscPolynomial:
+		glUniform2f(lscScale_, imgSize[0] / 1000.0, imgSize[1] / 1000.0);
+		glUniform3f(lsc0_,
+			    params.lscCoefficients[0].r(),
+			    params.lscCoefficients[0].g(),
+			    params.lscCoefficients[0].b());
+		glUniform3f(lsc1_,
+			    params.lscCoefficients[1].r(),
+			    params.lscCoefficients[1].g(),
+			    params.lscCoefficients[1].b());
+		glUniform3f(lsc2_,
+			    params.lscCoefficients[2].r(),
+			    params.lscCoefficients[2].g(),
+			    params.lscCoefficients[2].b());
+		break;
 	}
 
 	/*
diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
index d5cfcad1e..7d1fa31cb 100644
--- a/src/libcamera/software_isp/debayer_egl.h
+++ b/src/libcamera/software_isp/debayer_egl.h
@@ -95,6 +95,10 @@ private:
 	GLint textureUniformBayerDataIn_;
 
 	GLint textureUniformLsc_;
+	GLint lscScale_;
+	GLint lsc0_;
+	GLint lsc1_;
+	GLint lsc2_;
 
 	/* Represent per-frame CCM as a uniform vector of floats 3 x 3 */
 	GLint ccmUniformDataIn_;
