[v4,03/35] ipa: simple: awb: Port to use libipa AwbAlgorithm
diff mbox series

Message ID 20260708-libipa-algorithms-v4-3-f0e99f035294@ideasonboard.com
State New
Headers show
Series
  • ipa: libipa: Introduce libipa algorithms
Related show

Commit Message

Jacopo Mondi July 8, 2026, 9:24 a.m. UTC
From: Kieran Bingham <kieran.bingham@ideasonboard.com>

Port the SoftISP Awb algorithm to use the new libipa implementation
of AwbAlgorithm.

The awbAlgo_ class member is initialized with the Q<4, 8> type even if
there is no physical register representation for SoftISP.

The usage of libipa::awb::Context, which defines the gains vector as
RGB<double>, in the SimpleIPA ActiveState and FrameContext requires
changes to debayer_cpu.cpp and blc.cpp in order not to mix RGB<float>
and RGB<double>.

Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Tested-by: Robert Mader <robert.mader@collabora.com>
---
 .../internal/software_isp/debayer_params.h         |   4 +-
 src/ipa/simple/algorithms/awb.cpp                  | 131 +++++++++++++++------
 src/ipa/simple/algorithms/awb.h                    |  29 +++++
 src/ipa/simple/algorithms/blc.cpp                  |   2 +-
 src/ipa/simple/algorithms/ccm.cpp                  |   2 +-
 src/ipa/simple/ipa_context.h                       |  12 +-
 src/libcamera/software_isp/debayer_cpu.cpp         |  12 +-
 7 files changed, 141 insertions(+), 51 deletions(-)

Comments

Milan Zamazal July 8, 2026, 1:03 p.m. UTC | #1
Hi Jacopo,

Jacopo Mondi <jacopo.mondi@ideasonboard.com> writes:

> From: Kieran Bingham <kieran.bingham@ideasonboard.com>
>
> Port the SoftISP Awb algorithm to use the new libipa implementation
> of AwbAlgorithm.
>
> The awbAlgo_ class member is initialized with the Q<4, 8> type even if
> there is no physical register representation for SoftISP.
>
> The usage of libipa::awb::Context, which defines the gains vector as
> RGB<double>, in the SimpleIPA ActiveState and FrameContext requires
> changes to debayer_cpu.cpp and blc.cpp in order not to mix RGB<float>
> and RGB<double>.
>
> Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
> Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
> Tested-by: Robert Mader <robert.mader@collabora.com>
> ---
>  .../internal/software_isp/debayer_params.h         |   4 +-
>  src/ipa/simple/algorithms/awb.cpp                  | 131 +++++++++++++++------
>  src/ipa/simple/algorithms/awb.h                    |  29 +++++
>  src/ipa/simple/algorithms/blc.cpp                  |   2 +-
>  src/ipa/simple/algorithms/ccm.cpp                  |   2 +-
>  src/ipa/simple/ipa_context.h                       |  12 +-
>  src/libcamera/software_isp/debayer_cpu.cpp         |  12 +-
>  7 files changed, 141 insertions(+), 51 deletions(-)
>
> diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
> index 6772b43bced4..1074720d73c2 100644
> --- a/include/libcamera/internal/software_isp/debayer_params.h
> +++ b/include/libcamera/internal/software_isp/debayer_params.h
> @@ -21,10 +21,10 @@ struct DebayerParams {
>  	Matrix<float, 3, 3> combinedMatrix = { { 1.0, 0.0, 0.0,
>  						 0.0, 1.0, 0.0,
>  						 0.0, 0.0, 1.0 } };
> -	RGB<float> blackLevel = RGB<float>({ 0.0, 0.0, 0.0 });
> +	RGB<double> blackLevel = RGB<double>({ 0.0, 0.0, 0.0 });
>  	float gamma = 1.0;
>  	float contrastExp = 1.0;
> -	RGB<float> gains = RGB<float>({ 1.0, 1.0, 1.0 });
> +	RGB<double> gains = RGB<double>({ 1.0, 1.0, 1.0 });
>  };
>  
>  } /* namespace libcamera */
> diff --git a/src/ipa/simple/algorithms/awb.cpp b/src/ipa/simple/algorithms/awb.cpp
> index 05155c83d172..e8c965df65fa 100644
> --- a/src/ipa/simple/algorithms/awb.cpp
> +++ b/src/ipa/simple/algorithms/awb.cpp
> @@ -15,7 +15,6 @@
>  #include <libcamera/control_ids.h>
>  
>  #include "libipa/colours.h"
> -#include "simple/ipa_context.h"
>  
>  namespace libcamera {
>  
> @@ -23,41 +22,98 @@ LOG_DEFINE_CATEGORY(IPASoftAwb)
>  
>  namespace ipa::soft::algorithms {
>  
> +/*
> + * \todo Replace it with a proper Lux algorithm
> + */
> +static constexpr unsigned int kDefaultLux = 500;
> +
> +class SimpleAwbStats final : public AwbStats
> +{
> +public:
> +	SimpleAwbStats() = default;
> +
> +	SimpleAwbStats(const RGB<double> &rgbMeans)
> +	{
> +		rgbMeans_ = rgbMeans;

rgbMeans_ is undeclared and the compilation fails.  There are other
compilation issues in `simple' pipeline.  All regressions from v3,
please check.

> +		rg_ = rgbMeans_.r() / rgbMeans_.g();
> +		bg_ = rgbMeans_.b() / rgbMeans_.g();
> +	}
> +
> +	double computeColourError(const RGB<double> &gains) const override
> +	{
> +		/*
> +		 * Compute the sum of the squared colour error (non-greyness) as
> +		 * it appears in the log likelihood equation.
> +		 */
> +		double deltaR = gains.r() * rg_ - 1.0;
> +		double deltaB = gains.b() * bg_ - 1.0;
> +		double delta2 = deltaR * deltaR + deltaB * deltaB;
> +
> +		return delta2;
> +	}
> +
> +	bool valid() const override
> +	{
> +		/* Minimum mean value below which AWB can't operate. */
> +		constexpr double minValue = 0.2;
> +
> +		return rgbMeans_.r() > minValue || rgbMeans_.g() > minValue ||
> +		       rgbMeans_.b() > minValue;
> +	}
> +};
> +
> +/**
> + * \copydoc libcamera::ipa::Algorithm::init
> + */
> +int Awb::init(IPAContext &context, const ValueNode &tuningData)
> +{
> +	return awbAlgo_.init(tuningData, context.ctrlMap);
> +}
> +
> +/**
> + * \copydoc libcamera::ipa::Algorithm::configure
> + */
>  int Awb::configure(IPAContext &context,
>  		   [[maybe_unused]] const IPAConfigInfo &configInfo)
>  {
> -	auto &gains = context.activeState.awb.gains;
> -	gains = { { 1.0, 1.0, 1.0 } };
> +	return awbAlgo_.configure(context.activeState.awb);
> +}
>  
> -	return 0;
> +/**
> + * \copydoc libcamera::ipa::Algorithm::queueRequest
> + */
> +void Awb::queueRequest(IPAContext &context,
> +		       const uint32_t frame,
> +		       IPAFrameContext &frameContext,
> +		       const ControlList &controls)
> +{
> +	awbAlgo_.queueRequest(context.activeState.awb, frame, frameContext.awb,
> +			      controls);
>  }
>  
> +/**
> + * \copydoc libcamera::ipa::Algorithm::prepare
> + */
>  void Awb::prepare(IPAContext &context,
>  		  [[maybe_unused]] const uint32_t frame,
>  		  IPAFrameContext &frameContext,
>  		  DebayerParams *params)
>  {
> -	auto &gains = context.activeState.awb.gains;
> +	awbAlgo_.prepare(context.activeState.awb, frameContext.awb);
>  
> -	frameContext.gains = gains;
> -	params->gains = gains;
> +	params->gains = frameContext.awb.gains;
>  }
>  
> -void Awb::process(IPAContext &context,
> -		  [[maybe_unused]] const uint32_t frame,
> -		  IPAFrameContext &frameContext,
> -		  const SwIspStats *stats,
> -		  ControlList &metadata)
> +SimpleAwbStats Awb::calculateRgbMeans(IPAContext &context,
> +				      const SwIspStats *stats) const
>  {
> +	if (!stats->valid)
> +		return {};
> +
>  	const SwIspStats::Histogram &histogram = stats->yHistogram;
>  	const uint8_t blackLevel = context.activeState.blc.level;
>  
> -	metadata.set(controls::ColourGains, { frameContext.gains.r(),
> -					      frameContext.gains.b() });
> -
> -	if (!stats->valid)
> -		return;
> -
>  	/*
>  	 * Black level must be subtracted to get the correct AWB ratios, they
>  	 * would be off if they were computed from the whole brightness range
> @@ -67,30 +123,37 @@ void Awb::process(IPAContext &context,
>  		histogram.begin(), histogram.end(), uint64_t(0));
>  	const uint64_t offset = blackLevel * nPixels;
>  	const uint64_t minValid = 1;
> +
>  	/*
>  	 * Make sure the sums are at least minValid, while preventing unsigned
>  	 * integer underflow.
>  	 */
>  	const RGB<uint64_t> sum = stats->sum_.max(offset + minValid) - offset;
>  
> +	RGB<double> rgbMeans = { { static_cast<double>(sum.r() / nPixels),
> +				   static_cast<double>(sum.g() / nPixels),
> +				   static_cast<double>(sum.b() / nPixels) } };
> +
>  	/*
> -	 * Calculate red and blue gains for AWB.
> -	 * Clamp max gain at 4.0, this also avoids 0 division.
> +	 * \todo Determine the minimum allowed thresholds from the mean
> +	 * but we currently have the sum - not the mean value!
> +	 *
> +	 * Currently set to SimpleAwbStats::minColourValue() = 0.2.
>  	 */
> -	auto &gains = context.activeState.awb.gains;
> -	gains = { {
> -		sum.r() <= sum.g() / 4 ? 4.0f : static_cast<float>(sum.g()) / sum.r(),
> -		1.0,
> -		sum.b() <= sum.g() / 4 ? 4.0f : static_cast<float>(sum.g()) / sum.b(),
> -	} };
> -
> -	RGB<double> rgbGains{ { 1 / gains.r(), 1 / gains.g(), 1 / gains.b() } };
> -	context.activeState.awb.temperatureK = estimateCCT(rgbGains);
> -	metadata.set(controls::ColourTemperature, context.activeState.awb.temperatureK);
> -
> -	LOG(IPASoftAwb, Debug)
> -		<< "gain R/B: " << gains << "; temperature: "
> -		<< context.activeState.awb.temperatureK;
> +	return SimpleAwbStats(rgbMeans);
> +}
> +
> +/**
> + * \copydoc libcamera::ipa::Algorithm::process
> + */
> +void Awb::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
> +		  IPAFrameContext &frameContext, const SwIspStats *stats,
> +		  ControlList &metadata)
> +{
> +	SimpleAwbStats awbStats = calculateRgbMeans(context, stats);
> +
> +	awbAlgo_.process(context.activeState.awb, frameContext.awb, awbStats,
> +			 kDefaultLux, metadata);
>  }
>  
>  REGISTER_IPA_ALGORITHM(Awb, "Awb")
> diff --git a/src/ipa/simple/algorithms/awb.h b/src/ipa/simple/algorithms/awb.h
> index ad993f39c180..fec48c6eea1c 100644
> --- a/src/ipa/simple/algorithms/awb.h
> +++ b/src/ipa/simple/algorithms/awb.h
> @@ -7,19 +7,37 @@
>  
>  #pragma once
>  
> +#include <libcamera/controls.h>
> +
> +#include "libcamera/internal/software_isp/debayer_params.h"
> +#include "libcamera/internal/value_node.h"
> +
> +#include "libipa/awb.h"
> +#include "libipa/fixedpoint.h"
> +#include "simple/ipa_context.h"
> +
>  #include "algorithm.h"
>  
>  namespace libcamera {
>  
>  namespace ipa::soft::algorithms {
>  
> +class SimpleAwbStats;
> +
>  class Awb : public Algorithm
>  {
>  public:
>  	Awb() = default;
>  	~Awb() = default;
>  
> +	int init(IPAContext &context,
> +		 const ValueNode &tuningData) override;
>  	int configure(IPAContext &context, const IPAConfigInfo &configInfo) override;
> +
> +	void queueRequest(IPAContext &context,
> +			  [[maybe_unused]] const uint32_t frame,
> +			  IPAFrameContext &frameContext,
> +			  const ControlList &controls) override;
>  	void prepare(IPAContext &context,
>  		     const uint32_t frame,
>  		     IPAFrameContext &frameContext,
> @@ -29,6 +47,17 @@ public:
>  		     IPAFrameContext &frameContext,
>  		     const SwIspStats *stats,
>  		     ControlList &metadata) override;
> +
> +private:
> +	SimpleAwbStats calculateRgbMeans(IPAContext &context,
> +					 const SwIspStats *stats) const;
> +
> +	/*
> +	 * There actually is no Q register format for SoftISP, but allow the
> +	 * colour gains to range in the [0.0f, 3.999f] interval, which seems
> +	 * reasonable.
> +	 */
> +	AwbAlgorithm<UQ<2, 8>> awbAlgo_;
>  };
>  
>  } /* namespace ipa::soft::algorithms */
> diff --git a/src/ipa/simple/algorithms/blc.cpp b/src/ipa/simple/algorithms/blc.cpp
> index 677be56ed669..e45a913cd970 100644
> --- a/src/ipa/simple/algorithms/blc.cpp
> +++ b/src/ipa/simple/algorithms/blc.cpp
> @@ -53,7 +53,7 @@ void BlackLevel::prepare(IPAContext &context,
>  			 DebayerParams *params)
>  {
>  	/* Latch the blacklevel gain so GPUISP can apply. */
> -	params->blackLevel = RGB<float>(context.activeState.blc.level / 255.0f);
> +	params->blackLevel = RGB<double>(context.activeState.blc.level / 255.0f);
>  }
>  
>  void BlackLevel::process(IPAContext &context,
> diff --git a/src/ipa/simple/algorithms/ccm.cpp b/src/ipa/simple/algorithms/ccm.cpp
> index ace9c35dc462..ff37c718c6e4 100644
> --- a/src/ipa/simple/algorithms/ccm.cpp
> +++ b/src/ipa/simple/algorithms/ccm.cpp
> @@ -44,7 +44,7 @@ int Ccm::init([[maybe_unused]] IPAContext &context, const ValueNode &tuningData)
>  void Ccm::prepare(IPAContext &context, [[maybe_unused]] const uint32_t frame,
>  		  IPAFrameContext &frameContext, [[maybe_unused]] DebayerParams *params)
>  {
> -	const unsigned int ct = context.activeState.awb.temperatureK;
> +	const unsigned int ct = frameContext.awb.temperatureK;
>  
>  	/* Change CCM only on bigger temperature changes. */
>  	if (!currentCcm_ ||
> diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
> index 8ccfacb46a59..29643a655ce1 100644
> --- a/src/ipa/simple/ipa_context.h
> +++ b/src/ipa/simple/ipa_context.h
> @@ -16,6 +16,7 @@
>  #include "libcamera/internal/matrix.h"
>  #include "libcamera/internal/vector.h"
>  
> +#include <libipa/awb.h>
>  #include <libipa/fc_queue.h>
>  
>  #include "core_ipa_interface.h"
> @@ -36,6 +37,8 @@ struct IPASessionConfiguration {
>  };
>  
>  struct IPAActiveState {
> +	ipa::awb::ActiveState awb;
> +
>  	struct {
>  		int32_t exposure;
>  		double again;
> @@ -48,11 +51,6 @@ struct IPAActiveState {
>  		double lastGain;
>  	} blc;
>  
> -	struct {
> -		RGB<float> gains;
> -		unsigned int temperatureK;
> -	} awb;
> -
>  	Matrix<float, 3, 3> combinedMatrix;
>  
>  	struct {
> @@ -64,6 +62,8 @@ struct IPAActiveState {
>  };
>  
>  struct IPAFrameContext : public FrameContext {
> +	ipa::awb::FrameContext awb;
> +
>  	Matrix<float, 3, 3> ccm;
>  
>  	struct {
> @@ -71,8 +71,6 @@ struct IPAFrameContext : public FrameContext {
>  		double gain;
>  	} sensor;
>  
> -	RGB<float> gains;
> -
>  	float gamma;
>  	std::optional<float> contrast;
>  	std::optional<float> saturation;
> diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
> index 49382b4c2719..ab22635fdfaf 100644
> --- a/src/libcamera/software_isp/debayer_cpu.cpp
> +++ b/src/libcamera/software_isp/debayer_cpu.cpp
> @@ -1010,9 +1010,9 @@ void DebayerCpu::updateLookupTables(const DebayerParams &params)
>  	};
>  	const unsigned int gammaTableSize = gammaTable_.size();
>  
> -	const RGB<float> blackIndex = params.blackLevel * kRGBLookupSize;
> -	const RGB<float> gains = params.gains;
> -	const RGB<float> div = (RGB<float>(kRGBLookupSize) - blackIndex).max(1.0);
> +	const RGB<double> blackIndex = params.blackLevel * kRGBLookupSize;
> +	const RGB<double> gains = params.gains;
> +	const RGB<double> div = (RGB<double>(kRGBLookupSize) - blackIndex).max(1.0);
>  
>  	if (ccmEnabled_) {
>  		if (gammaUpdateNeeded ||
> @@ -1025,7 +1025,7 @@ void DebayerCpu::updateLookupTables(const DebayerParams &params)
>  			const unsigned int greenIndex = 1;
>  			const unsigned int blueIndex = swapRedBlueGains_ ? 0 : 2;
>  			for (unsigned int i = 0; i < kRGBLookupSize; i++) {
> -				const RGB<float> rgb = (gains * (RGB<float>(i) - blackIndex) * kRGBLookupSize / div)
> +				const RGB<double> rgb = (gains * (RGB<double>(i) - blackIndex) * kRGBLookupSize / div)
>  							       .clamp(0.0, kRGBLookupSize - 1);
>  				red[i].r = std::round(rgb.r() * params.combinedMatrix[redIndex][0]);
>  				red[i].g = std::round(rgb.r() * params.combinedMatrix[greenIndex][0]);
> @@ -1045,8 +1045,8 @@ void DebayerCpu::updateLookupTables(const DebayerParams &params)
>  			auto &green = green_;
>  			auto &blue = swapRedBlueGains_ ? red_ : blue_;
>  			for (unsigned int i = 0; i < kRGBLookupSize; i++) {
> -				const RGB<float> lutGains =
> -					(gains * (RGB<float>(i) - blackIndex) * gammaTableSize / div)
> +				const RGB<double> lutGains =
> +					(gains * (RGB<double>(i) - blackIndex) * gammaTableSize / div)
>  						.clamp(0.0, gammaTableSize - 1);
>  				red[i] = gammaTable_[lutGains.r()];
>  				green[i] = gammaTable_[lutGains.g()];

Patch
diff mbox series

diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
index 6772b43bced4..1074720d73c2 100644
--- a/include/libcamera/internal/software_isp/debayer_params.h
+++ b/include/libcamera/internal/software_isp/debayer_params.h
@@ -21,10 +21,10 @@  struct DebayerParams {
 	Matrix<float, 3, 3> combinedMatrix = { { 1.0, 0.0, 0.0,
 						 0.0, 1.0, 0.0,
 						 0.0, 0.0, 1.0 } };
-	RGB<float> blackLevel = RGB<float>({ 0.0, 0.0, 0.0 });
+	RGB<double> blackLevel = RGB<double>({ 0.0, 0.0, 0.0 });
 	float gamma = 1.0;
 	float contrastExp = 1.0;
-	RGB<float> gains = RGB<float>({ 1.0, 1.0, 1.0 });
+	RGB<double> gains = RGB<double>({ 1.0, 1.0, 1.0 });
 };
 
 } /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/awb.cpp b/src/ipa/simple/algorithms/awb.cpp
index 05155c83d172..e8c965df65fa 100644
--- a/src/ipa/simple/algorithms/awb.cpp
+++ b/src/ipa/simple/algorithms/awb.cpp
@@ -15,7 +15,6 @@ 
 #include <libcamera/control_ids.h>
 
 #include "libipa/colours.h"
-#include "simple/ipa_context.h"
 
 namespace libcamera {
 
@@ -23,41 +22,98 @@  LOG_DEFINE_CATEGORY(IPASoftAwb)
 
 namespace ipa::soft::algorithms {
 
+/*
+ * \todo Replace it with a proper Lux algorithm
+ */
+static constexpr unsigned int kDefaultLux = 500;
+
+class SimpleAwbStats final : public AwbStats
+{
+public:
+	SimpleAwbStats() = default;
+
+	SimpleAwbStats(const RGB<double> &rgbMeans)
+	{
+		rgbMeans_ = rgbMeans;
+
+		rg_ = rgbMeans_.r() / rgbMeans_.g();
+		bg_ = rgbMeans_.b() / rgbMeans_.g();
+	}
+
+	double computeColourError(const RGB<double> &gains) const override
+	{
+		/*
+		 * Compute the sum of the squared colour error (non-greyness) as
+		 * it appears in the log likelihood equation.
+		 */
+		double deltaR = gains.r() * rg_ - 1.0;
+		double deltaB = gains.b() * bg_ - 1.0;
+		double delta2 = deltaR * deltaR + deltaB * deltaB;
+
+		return delta2;
+	}
+
+	bool valid() const override
+	{
+		/* Minimum mean value below which AWB can't operate. */
+		constexpr double minValue = 0.2;
+
+		return rgbMeans_.r() > minValue || rgbMeans_.g() > minValue ||
+		       rgbMeans_.b() > minValue;
+	}
+};
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::init
+ */
+int Awb::init(IPAContext &context, const ValueNode &tuningData)
+{
+	return awbAlgo_.init(tuningData, context.ctrlMap);
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::configure
+ */
 int Awb::configure(IPAContext &context,
 		   [[maybe_unused]] const IPAConfigInfo &configInfo)
 {
-	auto &gains = context.activeState.awb.gains;
-	gains = { { 1.0, 1.0, 1.0 } };
+	return awbAlgo_.configure(context.activeState.awb);
+}
 
-	return 0;
+/**
+ * \copydoc libcamera::ipa::Algorithm::queueRequest
+ */
+void Awb::queueRequest(IPAContext &context,
+		       const uint32_t frame,
+		       IPAFrameContext &frameContext,
+		       const ControlList &controls)
+{
+	awbAlgo_.queueRequest(context.activeState.awb, frame, frameContext.awb,
+			      controls);
 }
 
+/**
+ * \copydoc libcamera::ipa::Algorithm::prepare
+ */
 void Awb::prepare(IPAContext &context,
 		  [[maybe_unused]] const uint32_t frame,
 		  IPAFrameContext &frameContext,
 		  DebayerParams *params)
 {
-	auto &gains = context.activeState.awb.gains;
+	awbAlgo_.prepare(context.activeState.awb, frameContext.awb);
 
-	frameContext.gains = gains;
-	params->gains = gains;
+	params->gains = frameContext.awb.gains;
 }
 
-void Awb::process(IPAContext &context,
-		  [[maybe_unused]] const uint32_t frame,
-		  IPAFrameContext &frameContext,
-		  const SwIspStats *stats,
-		  ControlList &metadata)
+SimpleAwbStats Awb::calculateRgbMeans(IPAContext &context,
+				      const SwIspStats *stats) const
 {
+	if (!stats->valid)
+		return {};
+
 	const SwIspStats::Histogram &histogram = stats->yHistogram;
 	const uint8_t blackLevel = context.activeState.blc.level;
 
-	metadata.set(controls::ColourGains, { frameContext.gains.r(),
-					      frameContext.gains.b() });
-
-	if (!stats->valid)
-		return;
-
 	/*
 	 * Black level must be subtracted to get the correct AWB ratios, they
 	 * would be off if they were computed from the whole brightness range
@@ -67,30 +123,37 @@  void Awb::process(IPAContext &context,
 		histogram.begin(), histogram.end(), uint64_t(0));
 	const uint64_t offset = blackLevel * nPixels;
 	const uint64_t minValid = 1;
+
 	/*
 	 * Make sure the sums are at least minValid, while preventing unsigned
 	 * integer underflow.
 	 */
 	const RGB<uint64_t> sum = stats->sum_.max(offset + minValid) - offset;
 
+	RGB<double> rgbMeans = { { static_cast<double>(sum.r() / nPixels),
+				   static_cast<double>(sum.g() / nPixels),
+				   static_cast<double>(sum.b() / nPixels) } };
+
 	/*
-	 * Calculate red and blue gains for AWB.
-	 * Clamp max gain at 4.0, this also avoids 0 division.
+	 * \todo Determine the minimum allowed thresholds from the mean
+	 * but we currently have the sum - not the mean value!
+	 *
+	 * Currently set to SimpleAwbStats::minColourValue() = 0.2.
 	 */
-	auto &gains = context.activeState.awb.gains;
-	gains = { {
-		sum.r() <= sum.g() / 4 ? 4.0f : static_cast<float>(sum.g()) / sum.r(),
-		1.0,
-		sum.b() <= sum.g() / 4 ? 4.0f : static_cast<float>(sum.g()) / sum.b(),
-	} };
-
-	RGB<double> rgbGains{ { 1 / gains.r(), 1 / gains.g(), 1 / gains.b() } };
-	context.activeState.awb.temperatureK = estimateCCT(rgbGains);
-	metadata.set(controls::ColourTemperature, context.activeState.awb.temperatureK);
-
-	LOG(IPASoftAwb, Debug)
-		<< "gain R/B: " << gains << "; temperature: "
-		<< context.activeState.awb.temperatureK;
+	return SimpleAwbStats(rgbMeans);
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::process
+ */
+void Awb::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
+		  IPAFrameContext &frameContext, const SwIspStats *stats,
+		  ControlList &metadata)
+{
+	SimpleAwbStats awbStats = calculateRgbMeans(context, stats);
+
+	awbAlgo_.process(context.activeState.awb, frameContext.awb, awbStats,
+			 kDefaultLux, metadata);
 }
 
 REGISTER_IPA_ALGORITHM(Awb, "Awb")
diff --git a/src/ipa/simple/algorithms/awb.h b/src/ipa/simple/algorithms/awb.h
index ad993f39c180..fec48c6eea1c 100644
--- a/src/ipa/simple/algorithms/awb.h
+++ b/src/ipa/simple/algorithms/awb.h
@@ -7,19 +7,37 @@ 
 
 #pragma once
 
+#include <libcamera/controls.h>
+
+#include "libcamera/internal/software_isp/debayer_params.h"
+#include "libcamera/internal/value_node.h"
+
+#include "libipa/awb.h"
+#include "libipa/fixedpoint.h"
+#include "simple/ipa_context.h"
+
 #include "algorithm.h"
 
 namespace libcamera {
 
 namespace ipa::soft::algorithms {
 
+class SimpleAwbStats;
+
 class Awb : public Algorithm
 {
 public:
 	Awb() = default;
 	~Awb() = default;
 
+	int init(IPAContext &context,
+		 const ValueNode &tuningData) override;
 	int configure(IPAContext &context, const IPAConfigInfo &configInfo) override;
+
+	void queueRequest(IPAContext &context,
+			  [[maybe_unused]] const uint32_t frame,
+			  IPAFrameContext &frameContext,
+			  const ControlList &controls) override;
 	void prepare(IPAContext &context,
 		     const uint32_t frame,
 		     IPAFrameContext &frameContext,
@@ -29,6 +47,17 @@  public:
 		     IPAFrameContext &frameContext,
 		     const SwIspStats *stats,
 		     ControlList &metadata) override;
+
+private:
+	SimpleAwbStats calculateRgbMeans(IPAContext &context,
+					 const SwIspStats *stats) const;
+
+	/*
+	 * There actually is no Q register format for SoftISP, but allow the
+	 * colour gains to range in the [0.0f, 3.999f] interval, which seems
+	 * reasonable.
+	 */
+	AwbAlgorithm<UQ<2, 8>> awbAlgo_;
 };
 
 } /* namespace ipa::soft::algorithms */
diff --git a/src/ipa/simple/algorithms/blc.cpp b/src/ipa/simple/algorithms/blc.cpp
index 677be56ed669..e45a913cd970 100644
--- a/src/ipa/simple/algorithms/blc.cpp
+++ b/src/ipa/simple/algorithms/blc.cpp
@@ -53,7 +53,7 @@  void BlackLevel::prepare(IPAContext &context,
 			 DebayerParams *params)
 {
 	/* Latch the blacklevel gain so GPUISP can apply. */
-	params->blackLevel = RGB<float>(context.activeState.blc.level / 255.0f);
+	params->blackLevel = RGB<double>(context.activeState.blc.level / 255.0f);
 }
 
 void BlackLevel::process(IPAContext &context,
diff --git a/src/ipa/simple/algorithms/ccm.cpp b/src/ipa/simple/algorithms/ccm.cpp
index ace9c35dc462..ff37c718c6e4 100644
--- a/src/ipa/simple/algorithms/ccm.cpp
+++ b/src/ipa/simple/algorithms/ccm.cpp
@@ -44,7 +44,7 @@  int Ccm::init([[maybe_unused]] IPAContext &context, const ValueNode &tuningData)
 void Ccm::prepare(IPAContext &context, [[maybe_unused]] const uint32_t frame,
 		  IPAFrameContext &frameContext, [[maybe_unused]] DebayerParams *params)
 {
-	const unsigned int ct = context.activeState.awb.temperatureK;
+	const unsigned int ct = frameContext.awb.temperatureK;
 
 	/* Change CCM only on bigger temperature changes. */
 	if (!currentCcm_ ||
diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
index 8ccfacb46a59..29643a655ce1 100644
--- a/src/ipa/simple/ipa_context.h
+++ b/src/ipa/simple/ipa_context.h
@@ -16,6 +16,7 @@ 
 #include "libcamera/internal/matrix.h"
 #include "libcamera/internal/vector.h"
 
+#include <libipa/awb.h>
 #include <libipa/fc_queue.h>
 
 #include "core_ipa_interface.h"
@@ -36,6 +37,8 @@  struct IPASessionConfiguration {
 };
 
 struct IPAActiveState {
+	ipa::awb::ActiveState awb;
+
 	struct {
 		int32_t exposure;
 		double again;
@@ -48,11 +51,6 @@  struct IPAActiveState {
 		double lastGain;
 	} blc;
 
-	struct {
-		RGB<float> gains;
-		unsigned int temperatureK;
-	} awb;
-
 	Matrix<float, 3, 3> combinedMatrix;
 
 	struct {
@@ -64,6 +62,8 @@  struct IPAActiveState {
 };
 
 struct IPAFrameContext : public FrameContext {
+	ipa::awb::FrameContext awb;
+
 	Matrix<float, 3, 3> ccm;
 
 	struct {
@@ -71,8 +71,6 @@  struct IPAFrameContext : public FrameContext {
 		double gain;
 	} sensor;
 
-	RGB<float> gains;
-
 	float gamma;
 	std::optional<float> contrast;
 	std::optional<float> saturation;
diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
index 49382b4c2719..ab22635fdfaf 100644
--- a/src/libcamera/software_isp/debayer_cpu.cpp
+++ b/src/libcamera/software_isp/debayer_cpu.cpp
@@ -1010,9 +1010,9 @@  void DebayerCpu::updateLookupTables(const DebayerParams &params)
 	};
 	const unsigned int gammaTableSize = gammaTable_.size();
 
-	const RGB<float> blackIndex = params.blackLevel * kRGBLookupSize;
-	const RGB<float> gains = params.gains;
-	const RGB<float> div = (RGB<float>(kRGBLookupSize) - blackIndex).max(1.0);
+	const RGB<double> blackIndex = params.blackLevel * kRGBLookupSize;
+	const RGB<double> gains = params.gains;
+	const RGB<double> div = (RGB<double>(kRGBLookupSize) - blackIndex).max(1.0);
 
 	if (ccmEnabled_) {
 		if (gammaUpdateNeeded ||
@@ -1025,7 +1025,7 @@  void DebayerCpu::updateLookupTables(const DebayerParams &params)
 			const unsigned int greenIndex = 1;
 			const unsigned int blueIndex = swapRedBlueGains_ ? 0 : 2;
 			for (unsigned int i = 0; i < kRGBLookupSize; i++) {
-				const RGB<float> rgb = (gains * (RGB<float>(i) - blackIndex) * kRGBLookupSize / div)
+				const RGB<double> rgb = (gains * (RGB<double>(i) - blackIndex) * kRGBLookupSize / div)
 							       .clamp(0.0, kRGBLookupSize - 1);
 				red[i].r = std::round(rgb.r() * params.combinedMatrix[redIndex][0]);
 				red[i].g = std::round(rgb.r() * params.combinedMatrix[greenIndex][0]);
@@ -1045,8 +1045,8 @@  void DebayerCpu::updateLookupTables(const DebayerParams &params)
 			auto &green = green_;
 			auto &blue = swapRedBlueGains_ ? red_ : blue_;
 			for (unsigned int i = 0; i < kRGBLookupSize; i++) {
-				const RGB<float> lutGains =
-					(gains * (RGB<float>(i) - blackIndex) * gammaTableSize / div)
+				const RGB<double> lutGains =
+					(gains * (RGB<double>(i) - blackIndex) * gammaTableSize / div)
 						.clamp(0.0, gammaTableSize - 1);
 				red[i] = gammaTable_[lutGains.r()];
 				green[i] = gammaTable_[lutGains.g()];