[v3,3/4] ipa: libipa: agc_mean_luminance: Change luminance target to piecewise linear function
diff mbox series

Message ID 20251119132221.2088013-4-stefan.klug@ideasonboard.com
State Accepted
Headers show
Series
  • libipa: agc: Fix constraints yTarget handling and add PWL support
Related show

Commit Message

Stefan Klug Nov. 19, 2025, 1:22 p.m. UTC
In some situations it is necessary to specify the target brightness
value depending on the overall lux level. Replace the float
relativeLuminanceTraget by a PWL. As the PWL loading code loads a plain
value as single point PWL, backwards compatibility to existing tuning
files is ensured.

While at it, order the class members in reverse xmas tree notation.

Signed-off-by: Paul Elder <paul.elder@ideasonboard.com>
Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>
Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>

---

Changes in v3:
- Dropped reference to original patch from commit message. For the
  record, the old patch was https://patchwork.libcamera.org/patch/20231/
- Ordered member variables in reverse xmas tree order
- Fallback to kDefaultRelativeLuminanceTarget if relativeLuminanceTraget
  is missing in the tuning file
- Warn once if lux level is not available after a few frames

Changes in v2:
- Collected tags
- Dropped special handling of plain values, as this is now part of the
  yaml PWL loader.
---
 src/ipa/libipa/agc_mean_luminance.cpp | 67 ++++++++++++++++++++++++---
 src/ipa/libipa/agc_mean_luminance.h   | 14 ++++--
 src/ipa/rkisp1/algorithms/agc.cpp     |  1 +
 3 files changed, 72 insertions(+), 10 deletions(-)

Comments

Kieran Bingham Nov. 19, 2025, 2:09 p.m. UTC | #1
Quoting Stefan Klug (2025-11-19 13:22:12)
> In some situations it is necessary to specify the target brightness
> value depending on the overall lux level. Replace the float
> relativeLuminanceTraget by a PWL. As the PWL loading code loads a plain
> value as single point PWL, backwards compatibility to existing tuning
> files is ensured.
> 
> While at it, order the class members in reverse xmas tree notation.
> 
> Signed-off-by: Paul Elder <paul.elder@ideasonboard.com>
> Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>
> Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>

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

> 
> ---
> 
> Changes in v3:
> - Dropped reference to original patch from commit message. For the
>   record, the old patch was https://patchwork.libcamera.org/patch/20231/
> - Ordered member variables in reverse xmas tree order
> - Fallback to kDefaultRelativeLuminanceTarget if relativeLuminanceTraget
>   is missing in the tuning file
> - Warn once if lux level is not available after a few frames
> 
> Changes in v2:
> - Collected tags
> - Dropped special handling of plain values, as this is now part of the
>   yaml PWL loader.
> ---
>  src/ipa/libipa/agc_mean_luminance.cpp | 67 ++++++++++++++++++++++++---
>  src/ipa/libipa/agc_mean_luminance.h   | 14 ++++--
>  src/ipa/rkisp1/algorithms/agc.cpp     |  1 +
>  3 files changed, 72 insertions(+), 10 deletions(-)
> 
> diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp
> index 64f36bd75dd2..4f2b5fad2082 100644
> --- a/src/ipa/libipa/agc_mean_luminance.cpp
> +++ b/src/ipa/libipa/agc_mean_luminance.cpp
> @@ -54,6 +54,14 @@ static constexpr double kDefaultRelativeLuminanceTarget = 0.16;
>   */
>  static constexpr double kMaxRelativeLuminanceTarget = 0.95;
>  
> +/*
> + * Default lux level
> + *
> + * If no lux level or a zero lux level is specified, but PWLs are used to
> + * specify luminance targets, this default level is used.
> + */
> +static constexpr unsigned int kDefaultLuxLevel = 500;
> +
>  /**
>   * \struct AgcMeanLuminance::AgcConstraint
>   * \brief The boundaries and target for an AeConstraintMode constraint
> @@ -144,17 +152,30 @@ static constexpr double kMaxRelativeLuminanceTarget = 0.95;
>   */
>  
>  AgcMeanLuminance::AgcMeanLuminance()
> -       : exposureCompensation_(1.0), frameCount_(0), filteredExposure_(0s),
> -         relativeLuminanceTarget_(0)
> +       : filteredExposure_(0s), luxWarningEnabled_(true),
> +         exposureCompensation_(1.0), frameCount_(0), lux_(0)
>  {
>  }
>  
>  AgcMeanLuminance::~AgcMeanLuminance() = default;
>  
> -void AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)
> +int AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)
>  {
> -       relativeLuminanceTarget_ =
> -               tuningData["relativeLuminanceTarget"].get<double>(kDefaultRelativeLuminanceTarget);
> +       auto &target = tuningData["relativeLuminanceTarget"];
> +       if (!target) {
> +               relativeLuminanceTarget_ = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } };
> +               return 0;
> +       }
> +
> +       auto pwl = target.get<Pwl>();
> +       if (!pwl) {
> +               LOG(AgcMeanLuminance, Error)
> +                       << "Failed to load relative luminance target.";
> +               return -EINVAL;
> +       }
> +
> +       relativeLuminanceTarget_ = std::move(*pwl);
> +       return 0;
>  }
>  
>  void AgcMeanLuminance::parseConstraint(const YamlObject &modeDict, int32_t id)
> @@ -325,6 +346,8 @@ void AgcMeanLuminance::configure(utils::Duration lineDuration,
>  {
>         for (auto &[id, helper] : exposureModeHelpers_)
>                 helper->configure(lineDuration, sensorHelper);
> +
> +       luxWarningEnabled_ = true;
>  }
>  
>  /**
> @@ -385,7 +408,9 @@ int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)
>  {
>         int ret;
>  
> -       parseRelativeLuminanceTarget(tuningData);
> +       ret = parseRelativeLuminanceTarget(tuningData);
> +       if (ret)
> +               return ret;
>  
>         ret = parseConstraintModes(tuningData);
>         if (ret)
> @@ -403,6 +428,16 @@ int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)
>   * AGC calculations. It is expressed as gain instead of EV.
>   */
>  
> +/**
> + * \fn AgcMeanLuminance::setLux(int lux)
> + * \brief Set the lux level
> + * \param[in] lux The lux level
> + *
> + * This function sets the lux level to be used in the AGC calculations. A value
> + * of 0 means no measurement and a default value of \a kDefaultLuxLevel is used
> + * if necessary.
> + */
> +
>  /**
>   * \brief Set the ExposureModeHelper limits for this class
>   * \param[in] minExposureTime Minimum exposure time to allow
> @@ -538,7 +573,25 @@ double AgcMeanLuminance::constraintClampGain(uint32_t constraintModeIndex,
>   */
>  double AgcMeanLuminance::effectiveYTarget() const
>  {
> -       return std::min(relativeLuminanceTarget_ * exposureCompensation_,
> +       double lux = lux_;
> +       if (relativeLuminanceTarget_.size() > 1 && lux_ == 0) {
> +               if (frameCount_ > 10 && luxWarningEnabled_) {
> +                       luxWarningEnabled_ = false;
> +                       LOG(AgcMeanLuminance, Warning)
> +                               << "Missing lux value for luminance target "
> +                                  "calculation, default to "
> +                               << kDefaultLuxLevel
> +                               << ". Note that the Lux algorithm must be "
> +                                  "included before the Agc algorithm.";
> +               }
> +
> +               lux = kDefaultLuxLevel;
> +       }
> +
> +       double luminanceTarget = relativeLuminanceTarget_.eval(
> +               relativeLuminanceTarget_.domain().clamp(lux));
> +
> +       return std::min(luminanceTarget * exposureCompensation_,
>                         kMaxRelativeLuminanceTarget);
>  }
>  
> diff --git a/src/ipa/libipa/agc_mean_luminance.h b/src/ipa/libipa/agc_mean_luminance.h
> index e5f164c3186b..746b97b16ffe 100644
> --- a/src/ipa/libipa/agc_mean_luminance.h
> +++ b/src/ipa/libipa/agc_mean_luminance.h
> @@ -20,6 +20,7 @@
>  
>  #include "exposure_mode_helper.h"
>  #include "histogram.h"
> +#include "pwl.h"
>  
>  namespace libcamera {
>  
> @@ -50,6 +51,11 @@ public:
>                 exposureCompensation_ = gain;
>         }
>  
> +       void setLux(unsigned int lux)
> +       {
> +               lux_ = lux;
> +       }
> +
>         void setLimits(utils::Duration minExposureTime, utils::Duration maxExposureTime,
>                        double minGain, double maxGain, std::vector<AgcConstraint> constraints);
>  
> @@ -82,7 +88,7 @@ public:
>  private:
>         virtual double estimateLuminance(const double gain) const = 0;
>  
> -       void parseRelativeLuminanceTarget(const YamlObject &tuningData);
> +       int parseRelativeLuminanceTarget(const YamlObject &tuningData);
>         void parseConstraint(const YamlObject &modeDict, int32_t id);
>         int parseConstraintModes(const YamlObject &tuningData);
>         int parseExposureModes(const YamlObject &tuningData);
> @@ -92,10 +98,12 @@ private:
>                                    double gain);
>         utils::Duration filterExposure(utils::Duration exposureValue);
>  
> +       utils::Duration filteredExposure_;
> +       mutable bool luxWarningEnabled_;
>         double exposureCompensation_;
> +       Pwl relativeLuminanceTarget_;
>         uint64_t frameCount_;
> -       utils::Duration filteredExposure_;
> -       double relativeLuminanceTarget_;
> +       unsigned int lux_;
>  
>         std::vector<AgcConstraint> additionalConstraints_;
>         std::map<int32_t, std::vector<AgcConstraint>> constraintModes_;
> diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp
> index f5a3c917cb69..1ecaff680978 100644
> --- a/src/ipa/rkisp1/algorithms/agc.cpp
> +++ b/src/ipa/rkisp1/algorithms/agc.cpp
> @@ -618,6 +618,7 @@ void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
>                 effectiveExposureValue *= frameContext.agc.quantizationGain;
>  
>         setExposureCompensation(pow(2.0, frameContext.agc.exposureValue));
> +       setLux(frameContext.lux.lux);
>  
>         utils::Duration newExposureTime;
>         double aGain, qGain, dGain;
> -- 
> 2.51.0
>
Jacopo Mondi Nov. 20, 2025, 5:05 p.m. UTC | #2
Hi Stefan

On Wed, Nov 19, 2025 at 02:22:12PM +0100, Stefan Klug wrote:
> In some situations it is necessary to specify the target brightness
> value depending on the overall lux level. Replace the float
> relativeLuminanceTraget by a PWL. As the PWL loading code loads a plain
> value as single point PWL, backwards compatibility to existing tuning
> files is ensured.
>
> While at it, order the class members in reverse xmas tree notation.
>
> Signed-off-by: Paul Elder <paul.elder@ideasonboard.com>
> Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>
> Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>
>
> ---
>
> Changes in v3:
> - Dropped reference to original patch from commit message. For the
>   record, the old patch was https://patchwork.libcamera.org/patch/20231/
> - Ordered member variables in reverse xmas tree order
> - Fallback to kDefaultRelativeLuminanceTarget if relativeLuminanceTraget
>   is missing in the tuning file
> - Warn once if lux level is not available after a few frames
>
> Changes in v2:
> - Collected tags
> - Dropped special handling of plain values, as this is now part of the
>   yaml PWL loader.
> ---
>  src/ipa/libipa/agc_mean_luminance.cpp | 67 ++++++++++++++++++++++++---
>  src/ipa/libipa/agc_mean_luminance.h   | 14 ++++--
>  src/ipa/rkisp1/algorithms/agc.cpp     |  1 +
>  3 files changed, 72 insertions(+), 10 deletions(-)
>
> diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp
> index 64f36bd75dd2..4f2b5fad2082 100644
> --- a/src/ipa/libipa/agc_mean_luminance.cpp
> +++ b/src/ipa/libipa/agc_mean_luminance.cpp
> @@ -54,6 +54,14 @@ static constexpr double kDefaultRelativeLuminanceTarget = 0.16;
>   */
>  static constexpr double kMaxRelativeLuminanceTarget = 0.95;
>
> +/*
> + * Default lux level
> + *
> + * If no lux level or a zero lux level is specified, but PWLs are used to
> + * specify luminance targets, this default level is used.
> + */
> +static constexpr unsigned int kDefaultLuxLevel = 500;
> +
>  /**
>   * \struct AgcMeanLuminance::AgcConstraint
>   * \brief The boundaries and target for an AeConstraintMode constraint
> @@ -144,17 +152,30 @@ static constexpr double kMaxRelativeLuminanceTarget = 0.95;
>   */
>
>  AgcMeanLuminance::AgcMeanLuminance()
> -	: exposureCompensation_(1.0), frameCount_(0), filteredExposure_(0s),
> -	  relativeLuminanceTarget_(0)
> +	: filteredExposure_(0s), luxWarningEnabled_(true),
> +	  exposureCompensation_(1.0), frameCount_(0), lux_(0)
>  {
>  }
>
>  AgcMeanLuminance::~AgcMeanLuminance() = default;
>
> -void AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)
> +int AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)
>  {
> -	relativeLuminanceTarget_ =
> -		tuningData["relativeLuminanceTarget"].get<double>(kDefaultRelativeLuminanceTarget);
> +	auto &target = tuningData["relativeLuminanceTarget"];
> +	if (!target) {
> +		relativeLuminanceTarget_ = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } };
> +		return 0;
> +	}
> +
> +	auto pwl = target.get<Pwl>();
> +	if (!pwl) {
> +		LOG(AgcMeanLuminance, Error)
> +			<< "Failed to load relative luminance target.";
> +		return -EINVAL;
> +	}
> +
> +	relativeLuminanceTarget_ = std::move(*pwl);
> +	return 0;
>  }
>
>  void AgcMeanLuminance::parseConstraint(const YamlObject &modeDict, int32_t id)
> @@ -325,6 +346,8 @@ void AgcMeanLuminance::configure(utils::Duration lineDuration,
>  {
>  	for (auto &[id, helper] : exposureModeHelpers_)
>  		helper->configure(lineDuration, sensorHelper);
> +
> +	luxWarningEnabled_ = true;
>  }
>
>  /**
> @@ -385,7 +408,9 @@ int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)
>  {
>  	int ret;
>
> -	parseRelativeLuminanceTarget(tuningData);
> +	ret = parseRelativeLuminanceTarget(tuningData);
> +	if (ret)
> +		return ret;
>
>  	ret = parseConstraintModes(tuningData);
>  	if (ret)
> @@ -403,6 +428,16 @@ int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)
>   * AGC calculations. It is expressed as gain instead of EV.
>   */
>
> +/**
> + * \fn AgcMeanLuminance::setLux(int lux)
> + * \brief Set the lux level
> + * \param[in] lux The lux level
> + *
> + * This function sets the lux level to be used in the AGC calculations. A value
> + * of 0 means no measurement and a default value of \a kDefaultLuxLevel is used
> + * if necessary.
> + */
> +
>  /**
>   * \brief Set the ExposureModeHelper limits for this class
>   * \param[in] minExposureTime Minimum exposure time to allow
> @@ -538,7 +573,25 @@ double AgcMeanLuminance::constraintClampGain(uint32_t constraintModeIndex,
>   */
>  double AgcMeanLuminance::effectiveYTarget() const
>  {
> -	return std::min(relativeLuminanceTarget_ * exposureCompensation_,
> +	double lux = lux_;
> +	if (relativeLuminanceTarget_.size() > 1 && lux_ == 0) {
> +		if (frameCount_ > 10 && luxWarningEnabled_) {

My intention here was to push for all the messages related to parsing
errors to be emitted once at startup, but I understand this is not
possible here as the error condition depends on two things:

1) relativeLuminanceTarget_ is specified as a PWL which associates a
lux level to a desired Y target
2) the IPA doesn't call setLux()

Now, while 1) is relatively easy to spot at parsing time 2) only
depend on the IPA implementation.

AgcMeanLuminance::effectiveYTarget() is called in 2 places by IPAs:

1) configure() where I guess it's ok not to have a lux level set yet ?
2) process() where it is expected to be always called after setLux() ?

if the above are true then I understand we should not Warn on
configure but we should Warn on the very first frame goes through
process for which effectiveYTarget() is called without a setLux() before?

I'm not sure I get why 10 is used, if that's the case.

Thanks
  j

> +			luxWarningEnabled_ = false;
> +			LOG(AgcMeanLuminance, Warning)
> +				<< "Missing lux value for luminance target "
> +				   "calculation, default to "
> +				<< kDefaultLuxLevel
> +				<< ". Note that the Lux algorithm must be "
> +				   "included before the Agc algorithm.";
> +		}
> +
> +		lux = kDefaultLuxLevel;
> +	}
> +
> +	double luminanceTarget = relativeLuminanceTarget_.eval(
> +		relativeLuminanceTarget_.domain().clamp(lux));
> +
> +	return std::min(luminanceTarget * exposureCompensation_,
>  			kMaxRelativeLuminanceTarget);
>  }
>
> diff --git a/src/ipa/libipa/agc_mean_luminance.h b/src/ipa/libipa/agc_mean_luminance.h
> index e5f164c3186b..746b97b16ffe 100644
> --- a/src/ipa/libipa/agc_mean_luminance.h
> +++ b/src/ipa/libipa/agc_mean_luminance.h
> @@ -20,6 +20,7 @@
>
>  #include "exposure_mode_helper.h"
>  #include "histogram.h"
> +#include "pwl.h"
>
>  namespace libcamera {
>
> @@ -50,6 +51,11 @@ public:
>  		exposureCompensation_ = gain;
>  	}
>
> +	void setLux(unsigned int lux)
> +	{
> +		lux_ = lux;
> +	}
> +
>  	void setLimits(utils::Duration minExposureTime, utils::Duration maxExposureTime,
>  		       double minGain, double maxGain, std::vector<AgcConstraint> constraints);
>
> @@ -82,7 +88,7 @@ public:
>  private:
>  	virtual double estimateLuminance(const double gain) const = 0;
>
> -	void parseRelativeLuminanceTarget(const YamlObject &tuningData);
> +	int parseRelativeLuminanceTarget(const YamlObject &tuningData);
>  	void parseConstraint(const YamlObject &modeDict, int32_t id);
>  	int parseConstraintModes(const YamlObject &tuningData);
>  	int parseExposureModes(const YamlObject &tuningData);
> @@ -92,10 +98,12 @@ private:
>  				   double gain);
>  	utils::Duration filterExposure(utils::Duration exposureValue);
>
> +	utils::Duration filteredExposure_;
> +	mutable bool luxWarningEnabled_;
>  	double exposureCompensation_;
> +	Pwl relativeLuminanceTarget_;
>  	uint64_t frameCount_;
> -	utils::Duration filteredExposure_;
> -	double relativeLuminanceTarget_;
> +	unsigned int lux_;
>
>  	std::vector<AgcConstraint> additionalConstraints_;
>  	std::map<int32_t, std::vector<AgcConstraint>> constraintModes_;
> diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp
> index f5a3c917cb69..1ecaff680978 100644
> --- a/src/ipa/rkisp1/algorithms/agc.cpp
> +++ b/src/ipa/rkisp1/algorithms/agc.cpp
> @@ -618,6 +618,7 @@ void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
>  		effectiveExposureValue *= frameContext.agc.quantizationGain;
>
>  	setExposureCompensation(pow(2.0, frameContext.agc.exposureValue));
> +	setLux(frameContext.lux.lux);
>
>  	utils::Duration newExposureTime;
>  	double aGain, qGain, dGain;
> --
> 2.51.0
>
Barnabás Pőcze Nov. 20, 2025, 5:16 p.m. UTC | #3
2025. 11. 20. 18:05 keltezéssel, Jacopo Mondi írta:
> Hi Stefan
> 
> On Wed, Nov 19, 2025 at 02:22:12PM +0100, Stefan Klug wrote:
>> In some situations it is necessary to specify the target brightness
>> value depending on the overall lux level. Replace the float
>> relativeLuminanceTraget by a PWL. As the PWL loading code loads a plain
>> value as single point PWL, backwards compatibility to existing tuning
>> files is ensured.
>>
>> While at it, order the class members in reverse xmas tree notation.
>>
>> Signed-off-by: Paul Elder <paul.elder@ideasonboard.com>
>> Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>
>> Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>
>>
>> ---
>>
>> Changes in v3:
>> - Dropped reference to original patch from commit message. For the
>>    record, the old patch was https://patchwork.libcamera.org/patch/20231/
>> - Ordered member variables in reverse xmas tree order
>> - Fallback to kDefaultRelativeLuminanceTarget if relativeLuminanceTraget
>>    is missing in the tuning file
>> - Warn once if lux level is not available after a few frames
>>
>> Changes in v2:
>> - Collected tags
>> - Dropped special handling of plain values, as this is now part of the
>>    yaml PWL loader.
>> ---
>>   src/ipa/libipa/agc_mean_luminance.cpp | 67 ++++++++++++++++++++++++---
>>   src/ipa/libipa/agc_mean_luminance.h   | 14 ++++--
>>   src/ipa/rkisp1/algorithms/agc.cpp     |  1 +
>>   3 files changed, 72 insertions(+), 10 deletions(-)
>>
>> diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp
>> index 64f36bd75dd2..4f2b5fad2082 100644
>> --- a/src/ipa/libipa/agc_mean_luminance.cpp
>> +++ b/src/ipa/libipa/agc_mean_luminance.cpp
>> @@ -54,6 +54,14 @@ static constexpr double kDefaultRelativeLuminanceTarget = 0.16;
>>    */
>>   static constexpr double kMaxRelativeLuminanceTarget = 0.95;
>>
>> +/*
>> + * Default lux level
>> + *
>> + * If no lux level or a zero lux level is specified, but PWLs are used to
>> + * specify luminance targets, this default level is used.
>> + */
>> +static constexpr unsigned int kDefaultLuxLevel = 500;
>> +
>>   /**
>>    * \struct AgcMeanLuminance::AgcConstraint
>>    * \brief The boundaries and target for an AeConstraintMode constraint
>> @@ -144,17 +152,30 @@ static constexpr double kMaxRelativeLuminanceTarget = 0.95;
>>    */
>>
>>   AgcMeanLuminance::AgcMeanLuminance()
>> -	: exposureCompensation_(1.0), frameCount_(0), filteredExposure_(0s),
>> -	  relativeLuminanceTarget_(0)
>> +	: filteredExposure_(0s), luxWarningEnabled_(true),
>> +	  exposureCompensation_(1.0), frameCount_(0), lux_(0)
>>   {
>>   }
>>
>>   AgcMeanLuminance::~AgcMeanLuminance() = default;
>>
>> -void AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)
>> +int AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)
>>   {
>> -	relativeLuminanceTarget_ =
>> -		tuningData["relativeLuminanceTarget"].get<double>(kDefaultRelativeLuminanceTarget);
>> +	auto &target = tuningData["relativeLuminanceTarget"];
>> +	if (!target) {
>> +		relativeLuminanceTarget_ = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } };
>> +		return 0;
>> +	}
>> +
>> +	auto pwl = target.get<Pwl>();
>> +	if (!pwl) {
>> +		LOG(AgcMeanLuminance, Error)
>> +			<< "Failed to load relative luminance target.";
>> +		return -EINVAL;
>> +	}
>> +
>> +	relativeLuminanceTarget_ = std::move(*pwl);
>> +	return 0;
>>   }
>>
>>   void AgcMeanLuminance::parseConstraint(const YamlObject &modeDict, int32_t id)
>> @@ -325,6 +346,8 @@ void AgcMeanLuminance::configure(utils::Duration lineDuration,
>>   {
>>   	for (auto &[id, helper] : exposureModeHelpers_)
>>   		helper->configure(lineDuration, sensorHelper);
>> +
>> +	luxWarningEnabled_ = true;
>>   }
>>
>>   /**
>> @@ -385,7 +408,9 @@ int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)
>>   {
>>   	int ret;
>>
>> -	parseRelativeLuminanceTarget(tuningData);
>> +	ret = parseRelativeLuminanceTarget(tuningData);
>> +	if (ret)
>> +		return ret;
>>
>>   	ret = parseConstraintModes(tuningData);
>>   	if (ret)
>> @@ -403,6 +428,16 @@ int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)
>>    * AGC calculations. It is expressed as gain instead of EV.
>>    */
>>
>> +/**
>> + * \fn AgcMeanLuminance::setLux(int lux)
>> + * \brief Set the lux level
>> + * \param[in] lux The lux level
>> + *
>> + * This function sets the lux level to be used in the AGC calculations. A value
>> + * of 0 means no measurement and a default value of \a kDefaultLuxLevel is used
>> + * if necessary.
>> + */
>> +
>>   /**
>>    * \brief Set the ExposureModeHelper limits for this class
>>    * \param[in] minExposureTime Minimum exposure time to allow
>> @@ -538,7 +573,25 @@ double AgcMeanLuminance::constraintClampGain(uint32_t constraintModeIndex,
>>    */
>>   double AgcMeanLuminance::effectiveYTarget() const
>>   {
>> -	return std::min(relativeLuminanceTarget_ * exposureCompensation_,
>> +	double lux = lux_;
>> +	if (relativeLuminanceTarget_.size() > 1 && lux_ == 0) {
>> +		if (frameCount_ > 10 && luxWarningEnabled_) {
> 
> My intention here was to push for all the messages related to parsing
> errors to be emitted once at startup, but I understand this is not
> possible here as the error condition depends on two things:
> 
> 1) relativeLuminanceTarget_ is specified as a PWL which associates a
> lux level to a desired Y target
> 2) the IPA doesn't call setLux()

Somewhat related, wouldn't it be better to have `lux` as a parameter of
`effectiveYTarget()`? (And hence `calculateNewEv()`, etc?)

It seems that both `lux` and `exposureCompensation` only need to be parameters
and need not be stored persistently in the class.


> 
> Now, while 1) is relatively easy to spot at parsing time 2) only
> depend on the IPA implementation.
> 
> AgcMeanLuminance::effectiveYTarget() is called in 2 places by IPAs:
> 
> 1) configure() where I guess it's ok not to have a lux level set yet ?
> 2) process() where it is expected to be always called after setLux() ?
> 
> if the above are true then I understand we should not Warn on
> configure but we should Warn on the very first frame goes through
> process for which effectiveYTarget() is called without a setLux() before?
> 
> I'm not sure I get why 10 is used, if that's the case.
> 
> Thanks
>    j
> 
>> +			luxWarningEnabled_ = false;
>> +			LOG(AgcMeanLuminance, Warning)
>> +				<< "Missing lux value for luminance target "
>> +				   "calculation, default to "
>> +				<< kDefaultLuxLevel
>> +				<< ". Note that the Lux algorithm must be "
>> +				   "included before the Agc algorithm.";
>> +		}
>> +
>> +		lux = kDefaultLuxLevel;
>> +	}
>> +
>> +	double luminanceTarget = relativeLuminanceTarget_.eval(
>> +		relativeLuminanceTarget_.domain().clamp(lux));
>> +
>> +	return std::min(luminanceTarget * exposureCompensation_,
>>   			kMaxRelativeLuminanceTarget);
>>   }
>>
>> diff --git a/src/ipa/libipa/agc_mean_luminance.h b/src/ipa/libipa/agc_mean_luminance.h
>> index e5f164c3186b..746b97b16ffe 100644
>> --- a/src/ipa/libipa/agc_mean_luminance.h
>> +++ b/src/ipa/libipa/agc_mean_luminance.h
>> @@ -20,6 +20,7 @@
>>
>>   #include "exposure_mode_helper.h"
>>   #include "histogram.h"
>> +#include "pwl.h"
>>
>>   namespace libcamera {
>>
>> @@ -50,6 +51,11 @@ public:
>>   		exposureCompensation_ = gain;
>>   	}
>>
>> +	void setLux(unsigned int lux)
>> +	{
>> +		lux_ = lux;
>> +	}
>> +
>>   	void setLimits(utils::Duration minExposureTime, utils::Duration maxExposureTime,
>>   		       double minGain, double maxGain, std::vector<AgcConstraint> constraints);
>>
>> @@ -82,7 +88,7 @@ public:
>>   private:
>>   	virtual double estimateLuminance(const double gain) const = 0;
>>
>> -	void parseRelativeLuminanceTarget(const YamlObject &tuningData);
>> +	int parseRelativeLuminanceTarget(const YamlObject &tuningData);
>>   	void parseConstraint(const YamlObject &modeDict, int32_t id);
>>   	int parseConstraintModes(const YamlObject &tuningData);
>>   	int parseExposureModes(const YamlObject &tuningData);
>> @@ -92,10 +98,12 @@ private:
>>   				   double gain);
>>   	utils::Duration filterExposure(utils::Duration exposureValue);
>>
>> +	utils::Duration filteredExposure_;
>> +	mutable bool luxWarningEnabled_;
>>   	double exposureCompensation_;
>> +	Pwl relativeLuminanceTarget_;
>>   	uint64_t frameCount_;
>> -	utils::Duration filteredExposure_;
>> -	double relativeLuminanceTarget_;
>> +	unsigned int lux_;
>>
>>   	std::vector<AgcConstraint> additionalConstraints_;
>>   	std::map<int32_t, std::vector<AgcConstraint>> constraintModes_;
>> diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp
>> index f5a3c917cb69..1ecaff680978 100644
>> --- a/src/ipa/rkisp1/algorithms/agc.cpp
>> +++ b/src/ipa/rkisp1/algorithms/agc.cpp
>> @@ -618,6 +618,7 @@ void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
>>   		effectiveExposureValue *= frameContext.agc.quantizationGain;
>>
>>   	setExposureCompensation(pow(2.0, frameContext.agc.exposureValue));
>> +	setLux(frameContext.lux.lux);
>>
>>   	utils::Duration newExposureTime;
>>   	double aGain, qGain, dGain;
>> --
>> 2.51.0
>>
Stefan Klug Nov. 20, 2025, 5:54 p.m. UTC | #4
Hi Jacopo,

Quoting Jacopo Mondi (2025-11-20 18:05:41)
> Hi Stefan
> 
> On Wed, Nov 19, 2025 at 02:22:12PM +0100, Stefan Klug wrote:
> > In some situations it is necessary to specify the target brightness
> > value depending on the overall lux level. Replace the float
> > relativeLuminanceTraget by a PWL. As the PWL loading code loads a plain
> > value as single point PWL, backwards compatibility to existing tuning
> > files is ensured.
> >
> > While at it, order the class members in reverse xmas tree notation.
> >
> > Signed-off-by: Paul Elder <paul.elder@ideasonboard.com>
> > Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>
> > Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>
> >
> > ---
> >
> > Changes in v3:
> > - Dropped reference to original patch from commit message. For the
> >   record, the old patch was https://patchwork.libcamera.org/patch/20231/
> > - Ordered member variables in reverse xmas tree order
> > - Fallback to kDefaultRelativeLuminanceTarget if relativeLuminanceTraget
> >   is missing in the tuning file
> > - Warn once if lux level is not available after a few frames
> >
> > Changes in v2:
> > - Collected tags
> > - Dropped special handling of plain values, as this is now part of the
> >   yaml PWL loader.
> > ---
> >  src/ipa/libipa/agc_mean_luminance.cpp | 67 ++++++++++++++++++++++++---
> >  src/ipa/libipa/agc_mean_luminance.h   | 14 ++++--
> >  src/ipa/rkisp1/algorithms/agc.cpp     |  1 +
> >  3 files changed, 72 insertions(+), 10 deletions(-)
> >
> > diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp
> > index 64f36bd75dd2..4f2b5fad2082 100644
> > --- a/src/ipa/libipa/agc_mean_luminance.cpp
> > +++ b/src/ipa/libipa/agc_mean_luminance.cpp
> > @@ -54,6 +54,14 @@ static constexpr double kDefaultRelativeLuminanceTarget = 0.16;
> >   */
> >  static constexpr double kMaxRelativeLuminanceTarget = 0.95;
> >
> > +/*
> > + * Default lux level
> > + *
> > + * If no lux level or a zero lux level is specified, but PWLs are used to
> > + * specify luminance targets, this default level is used.
> > + */
> > +static constexpr unsigned int kDefaultLuxLevel = 500;
> > +
> >  /**
> >   * \struct AgcMeanLuminance::AgcConstraint
> >   * \brief The boundaries and target for an AeConstraintMode constraint
> > @@ -144,17 +152,30 @@ static constexpr double kMaxRelativeLuminanceTarget = 0.95;
> >   */
> >
> >  AgcMeanLuminance::AgcMeanLuminance()
> > -     : exposureCompensation_(1.0), frameCount_(0), filteredExposure_(0s),
> > -       relativeLuminanceTarget_(0)
> > +     : filteredExposure_(0s), luxWarningEnabled_(true),
> > +       exposureCompensation_(1.0), frameCount_(0), lux_(0)
> >  {
> >  }
> >
> >  AgcMeanLuminance::~AgcMeanLuminance() = default;
> >
> > -void AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)
> > +int AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)
> >  {
> > -     relativeLuminanceTarget_ =
> > -             tuningData["relativeLuminanceTarget"].get<double>(kDefaultRelativeLuminanceTarget);
> > +     auto &target = tuningData["relativeLuminanceTarget"];
> > +     if (!target) {
> > +             relativeLuminanceTarget_ = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } };
> > +             return 0;
> > +     }
> > +
> > +     auto pwl = target.get<Pwl>();
> > +     if (!pwl) {
> > +             LOG(AgcMeanLuminance, Error)
> > +                     << "Failed to load relative luminance target.";
> > +             return -EINVAL;
> > +     }
> > +
> > +     relativeLuminanceTarget_ = std::move(*pwl);
> > +     return 0;
> >  }
> >
> >  void AgcMeanLuminance::parseConstraint(const YamlObject &modeDict, int32_t id)
> > @@ -325,6 +346,8 @@ void AgcMeanLuminance::configure(utils::Duration lineDuration,
> >  {
> >       for (auto &[id, helper] : exposureModeHelpers_)
> >               helper->configure(lineDuration, sensorHelper);
> > +
> > +     luxWarningEnabled_ = true;
> >  }
> >
> >  /**
> > @@ -385,7 +408,9 @@ int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)
> >  {
> >       int ret;
> >
> > -     parseRelativeLuminanceTarget(tuningData);
> > +     ret = parseRelativeLuminanceTarget(tuningData);
> > +     if (ret)
> > +             return ret;
> >
> >       ret = parseConstraintModes(tuningData);
> >       if (ret)
> > @@ -403,6 +428,16 @@ int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)
> >   * AGC calculations. It is expressed as gain instead of EV.
> >   */
> >
> > +/**
> > + * \fn AgcMeanLuminance::setLux(int lux)
> > + * \brief Set the lux level
> > + * \param[in] lux The lux level
> > + *
> > + * This function sets the lux level to be used in the AGC calculations. A value
> > + * of 0 means no measurement and a default value of \a kDefaultLuxLevel is used
> > + * if necessary.
> > + */
> > +
> >  /**
> >   * \brief Set the ExposureModeHelper limits for this class
> >   * \param[in] minExposureTime Minimum exposure time to allow
> > @@ -538,7 +573,25 @@ double AgcMeanLuminance::constraintClampGain(uint32_t constraintModeIndex,
> >   */
> >  double AgcMeanLuminance::effectiveYTarget() const
> >  {
> > -     return std::min(relativeLuminanceTarget_ * exposureCompensation_,
> > +     double lux = lux_;
> > +     if (relativeLuminanceTarget_.size() > 1 && lux_ == 0) {
> > +             if (frameCount_ > 10 && luxWarningEnabled_) {
> 
> My intention here was to push for all the messages related to parsing
> errors to be emitted once at startup, but I understand this is not
> possible here as the error condition depends on two things:
> 
> 1) relativeLuminanceTarget_ is specified as a PWL which associates a
> lux level to a desired Y target
> 2) the IPA doesn't call setLux()
> 
> Now, while 1) is relatively easy to spot at parsing time 2) only
> depend on the IPA implementation.
> 
> AgcMeanLuminance::effectiveYTarget() is called in 2 places by IPAs:
> 
> 1) configure() where I guess it's ok not to have a lux level set yet ?
> 2) process() where it is expected to be always called after setLux() ?
> 
> if the above are true then I understand we should not Warn on
> configure but we should Warn on the very first frame goes through
> process for which effectiveYTarget() is called without a setLux() before?
> 
> I'm not sure I get why 10 is used, if that's the case.

10 was arbitrarily chosen, so that we get at least something. 
Reading your message I realize, that I was a few steps further and
already planning for the regulation rework which makes this hard to
follow.

You are right, currently it is called within process() where the agc
calculation happens. The issue here is that this is plain wrong but
fixing needs to wait until after the regulation series.

The agc calculation and also setLux() should happen in prepare(). Reason
is that for example frameContext.agc.exposureValue feeds into the agc
calculations. So we get stats for frame n, calculate new exposure
settings based on fc[n].agc.exposureValue and these will then be used in
prepare(n+lookahead). That is wrong.

The correct thing to do would be to cache the stats but do the agc
calculations in prepare() so that fc[n+lookahed].agc.exposureValue feeds
into the calculation.

When the above is fixed, there will be no lux value available for the
first few frames as prepare() is run for these frames before the stats
for frame 0 have arrived which is the time when the first lux value can
be calculated.

10 is bigger than the number of frames that get prepared in advance and
is small enough to show up early on.

Does that make sense?

Best regards,
Stefan

> 
> Thanks
>   j
> 
> > +                     luxWarningEnabled_ = false;
> > +                     LOG(AgcMeanLuminance, Warning)
> > +                             << "Missing lux value for luminance target "
> > +                                "calculation, default to "
> > +                             << kDefaultLuxLevel
> > +                             << ". Note that the Lux algorithm must be "
> > +                                "included before the Agc algorithm.";
> > +             }
> > +
> > +             lux = kDefaultLuxLevel;
> > +     }
> > +
> > +     double luminanceTarget = relativeLuminanceTarget_.eval(
> > +             relativeLuminanceTarget_.domain().clamp(lux));
> > +
> > +     return std::min(luminanceTarget * exposureCompensation_,
> >                       kMaxRelativeLuminanceTarget);
> >  }
> >
> > diff --git a/src/ipa/libipa/agc_mean_luminance.h b/src/ipa/libipa/agc_mean_luminance.h
> > index e5f164c3186b..746b97b16ffe 100644
> > --- a/src/ipa/libipa/agc_mean_luminance.h
> > +++ b/src/ipa/libipa/agc_mean_luminance.h
> > @@ -20,6 +20,7 @@
> >
> >  #include "exposure_mode_helper.h"
> >  #include "histogram.h"
> > +#include "pwl.h"
> >
> >  namespace libcamera {
> >
> > @@ -50,6 +51,11 @@ public:
> >               exposureCompensation_ = gain;
> >       }
> >
> > +     void setLux(unsigned int lux)
> > +     {
> > +             lux_ = lux;
> > +     }
> > +
> >       void setLimits(utils::Duration minExposureTime, utils::Duration maxExposureTime,
> >                      double minGain, double maxGain, std::vector<AgcConstraint> constraints);
> >
> > @@ -82,7 +88,7 @@ public:
> >  private:
> >       virtual double estimateLuminance(const double gain) const = 0;
> >
> > -     void parseRelativeLuminanceTarget(const YamlObject &tuningData);
> > +     int parseRelativeLuminanceTarget(const YamlObject &tuningData);
> >       void parseConstraint(const YamlObject &modeDict, int32_t id);
> >       int parseConstraintModes(const YamlObject &tuningData);
> >       int parseExposureModes(const YamlObject &tuningData);
> > @@ -92,10 +98,12 @@ private:
> >                                  double gain);
> >       utils::Duration filterExposure(utils::Duration exposureValue);
> >
> > +     utils::Duration filteredExposure_;
> > +     mutable bool luxWarningEnabled_;
> >       double exposureCompensation_;
> > +     Pwl relativeLuminanceTarget_;
> >       uint64_t frameCount_;
> > -     utils::Duration filteredExposure_;
> > -     double relativeLuminanceTarget_;
> > +     unsigned int lux_;
> >
> >       std::vector<AgcConstraint> additionalConstraints_;
> >       std::map<int32_t, std::vector<AgcConstraint>> constraintModes_;
> > diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp
> > index f5a3c917cb69..1ecaff680978 100644
> > --- a/src/ipa/rkisp1/algorithms/agc.cpp
> > +++ b/src/ipa/rkisp1/algorithms/agc.cpp
> > @@ -618,6 +618,7 @@ void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
> >               effectiveExposureValue *= frameContext.agc.quantizationGain;
> >
> >       setExposureCompensation(pow(2.0, frameContext.agc.exposureValue));
> > +     setLux(frameContext.lux.lux);
> >
> >       utils::Duration newExposureTime;
> >       double aGain, qGain, dGain;
> > --
> > 2.51.0
> >
Jacopo Mondi Nov. 21, 2025, 9:04 a.m. UTC | #5
Hi Stefan

On Thu, Nov 20, 2025 at 06:54:28PM +0100, Stefan Klug wrote:
> Hi Jacopo,
>
> Quoting Jacopo Mondi (2025-11-20 18:05:41)
> > Hi Stefan
> >
> > On Wed, Nov 19, 2025 at 02:22:12PM +0100, Stefan Klug wrote:
> > > In some situations it is necessary to specify the target brightness
> > > value depending on the overall lux level. Replace the float
> > > relativeLuminanceTraget by a PWL. As the PWL loading code loads a plain
> > > value as single point PWL, backwards compatibility to existing tuning
> > > files is ensured.
> > >
> > > While at it, order the class members in reverse xmas tree notation.
> > >
> > > Signed-off-by: Paul Elder <paul.elder@ideasonboard.com>
> > > Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>
> > > Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>
> > >
> > > ---
> > >
> > > Changes in v3:
> > > - Dropped reference to original patch from commit message. For the
> > >   record, the old patch was https://patchwork.libcamera.org/patch/20231/
> > > - Ordered member variables in reverse xmas tree order
> > > - Fallback to kDefaultRelativeLuminanceTarget if relativeLuminanceTraget
> > >   is missing in the tuning file
> > > - Warn once if lux level is not available after a few frames
> > >
> > > Changes in v2:
> > > - Collected tags
> > > - Dropped special handling of plain values, as this is now part of the
> > >   yaml PWL loader.
> > > ---
> > >  src/ipa/libipa/agc_mean_luminance.cpp | 67 ++++++++++++++++++++++++---
> > >  src/ipa/libipa/agc_mean_luminance.h   | 14 ++++--
> > >  src/ipa/rkisp1/algorithms/agc.cpp     |  1 +
> > >  3 files changed, 72 insertions(+), 10 deletions(-)
> > >
> > > diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp
> > > index 64f36bd75dd2..4f2b5fad2082 100644
> > > --- a/src/ipa/libipa/agc_mean_luminance.cpp
> > > +++ b/src/ipa/libipa/agc_mean_luminance.cpp
> > > @@ -54,6 +54,14 @@ static constexpr double kDefaultRelativeLuminanceTarget = 0.16;
> > >   */
> > >  static constexpr double kMaxRelativeLuminanceTarget = 0.95;
> > >
> > > +/*
> > > + * Default lux level
> > > + *
> > > + * If no lux level or a zero lux level is specified, but PWLs are used to
> > > + * specify luminance targets, this default level is used.
> > > + */
> > > +static constexpr unsigned int kDefaultLuxLevel = 500;
> > > +
> > >  /**
> > >   * \struct AgcMeanLuminance::AgcConstraint
> > >   * \brief The boundaries and target for an AeConstraintMode constraint
> > > @@ -144,17 +152,30 @@ static constexpr double kMaxRelativeLuminanceTarget = 0.95;
> > >   */
> > >
> > >  AgcMeanLuminance::AgcMeanLuminance()
> > > -     : exposureCompensation_(1.0), frameCount_(0), filteredExposure_(0s),
> > > -       relativeLuminanceTarget_(0)
> > > +     : filteredExposure_(0s), luxWarningEnabled_(true),
> > > +       exposureCompensation_(1.0), frameCount_(0), lux_(0)
> > >  {
> > >  }
> > >
> > >  AgcMeanLuminance::~AgcMeanLuminance() = default;
> > >
> > > -void AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)
> > > +int AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)
> > >  {
> > > -     relativeLuminanceTarget_ =
> > > -             tuningData["relativeLuminanceTarget"].get<double>(kDefaultRelativeLuminanceTarget);
> > > +     auto &target = tuningData["relativeLuminanceTarget"];
> > > +     if (!target) {
> > > +             relativeLuminanceTarget_ = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } };
> > > +             return 0;
> > > +     }
> > > +
> > > +     auto pwl = target.get<Pwl>();
> > > +     if (!pwl) {
> > > +             LOG(AgcMeanLuminance, Error)
> > > +                     << "Failed to load relative luminance target.";
> > > +             return -EINVAL;
> > > +     }
> > > +
> > > +     relativeLuminanceTarget_ = std::move(*pwl);
> > > +     return 0;
> > >  }
> > >
> > >  void AgcMeanLuminance::parseConstraint(const YamlObject &modeDict, int32_t id)
> > > @@ -325,6 +346,8 @@ void AgcMeanLuminance::configure(utils::Duration lineDuration,
> > >  {
> > >       for (auto &[id, helper] : exposureModeHelpers_)
> > >               helper->configure(lineDuration, sensorHelper);
> > > +
> > > +     luxWarningEnabled_ = true;
> > >  }
> > >
> > >  /**
> > > @@ -385,7 +408,9 @@ int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)
> > >  {
> > >       int ret;
> > >
> > > -     parseRelativeLuminanceTarget(tuningData);
> > > +     ret = parseRelativeLuminanceTarget(tuningData);
> > > +     if (ret)
> > > +             return ret;
> > >
> > >       ret = parseConstraintModes(tuningData);
> > >       if (ret)
> > > @@ -403,6 +428,16 @@ int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)
> > >   * AGC calculations. It is expressed as gain instead of EV.
> > >   */
> > >
> > > +/**
> > > + * \fn AgcMeanLuminance::setLux(int lux)
> > > + * \brief Set the lux level
> > > + * \param[in] lux The lux level
> > > + *
> > > + * This function sets the lux level to be used in the AGC calculations. A value
> > > + * of 0 means no measurement and a default value of \a kDefaultLuxLevel is used
> > > + * if necessary.
> > > + */
> > > +
> > >  /**
> > >   * \brief Set the ExposureModeHelper limits for this class
> > >   * \param[in] minExposureTime Minimum exposure time to allow
> > > @@ -538,7 +573,25 @@ double AgcMeanLuminance::constraintClampGain(uint32_t constraintModeIndex,
> > >   */
> > >  double AgcMeanLuminance::effectiveYTarget() const
> > >  {
> > > -     return std::min(relativeLuminanceTarget_ * exposureCompensation_,
> > > +     double lux = lux_;
> > > +     if (relativeLuminanceTarget_.size() > 1 && lux_ == 0) {
> > > +             if (frameCount_ > 10 && luxWarningEnabled_) {
> >
> > My intention here was to push for all the messages related to parsing
> > errors to be emitted once at startup, but I understand this is not
> > possible here as the error condition depends on two things:
> >
> > 1) relativeLuminanceTarget_ is specified as a PWL which associates a
> > lux level to a desired Y target
> > 2) the IPA doesn't call setLux()
> >
> > Now, while 1) is relatively easy to spot at parsing time 2) only
> > depend on the IPA implementation.
> >
> > AgcMeanLuminance::effectiveYTarget() is called in 2 places by IPAs:
> >
> > 1) configure() where I guess it's ok not to have a lux level set yet ?
> > 2) process() where it is expected to be always called after setLux() ?
> >
> > if the above are true then I understand we should not Warn on
> > configure but we should Warn on the very first frame goes through
> > process for which effectiveYTarget() is called without a setLux() before?
> >
> > I'm not sure I get why 10 is used, if that's the case.
>
> 10 was arbitrarily chosen, so that we get at least something.
> Reading your message I realize, that I was a few steps further and
> already planning for the regulation rework which makes this hard to
> follow.
>
> You are right, currently it is called within process() where the agc
> calculation happens. The issue here is that this is plain wrong but
> fixing needs to wait until after the regulation series.
>
> The agc calculation and also setLux() should happen in prepare(). Reason
> is that for example frameContext.agc.exposureValue feeds into the agc
> calculations. So we get stats for frame n, calculate new exposure
> settings based on fc[n].agc.exposureValue and these will then be used in
> prepare(n+lookahead). That is wrong.
>
> The correct thing to do would be to cache the stats but do the agc
> calculations in prepare() so that fc[n+lookahed].agc.exposureValue feeds
> into the calculation.
>
> When the above is fixed, there will be no lux value available for the
> first few frames as prepare() is run for these frames before the stats
> for frame 0 have arrived which is the time when the first lux value can
> be calculated.

Couldn't we then avoid all error condition by:

- coalesce setLux() in effectiveYTarget() as Barnabas suggested by
  making the Lux value an (optional<>?) parameter
- use a default Lux value in the caller until we don't get a real lux
  value OR pass nullopt

This will make mandatory for the IPA to pass a lux in. Is it something
that you would expect ?

>
> 10 is bigger than the number of frames that get prepared in advance and
> is small enough to show up early on.
>
> Does that make sense?
>
> Best regards,
> Stefan
>
> >
> > Thanks
> >   j
> >
> > > +                     luxWarningEnabled_ = false;
> > > +                     LOG(AgcMeanLuminance, Warning)
> > > +                             << "Missing lux value for luminance target "
> > > +                                "calculation, default to "
> > > +                             << kDefaultLuxLevel
> > > +                             << ". Note that the Lux algorithm must be "
> > > +                                "included before the Agc algorithm.";
> > > +             }
> > > +
> > > +             lux = kDefaultLuxLevel;
> > > +     }
> > > +
> > > +     double luminanceTarget = relativeLuminanceTarget_.eval(
> > > +             relativeLuminanceTarget_.domain().clamp(lux));
> > > +
> > > +     return std::min(luminanceTarget * exposureCompensation_,
> > >                       kMaxRelativeLuminanceTarget);
> > >  }
> > >
> > > diff --git a/src/ipa/libipa/agc_mean_luminance.h b/src/ipa/libipa/agc_mean_luminance.h
> > > index e5f164c3186b..746b97b16ffe 100644
> > > --- a/src/ipa/libipa/agc_mean_luminance.h
> > > +++ b/src/ipa/libipa/agc_mean_luminance.h
> > > @@ -20,6 +20,7 @@
> > >
> > >  #include "exposure_mode_helper.h"
> > >  #include "histogram.h"
> > > +#include "pwl.h"
> > >
> > >  namespace libcamera {
> > >
> > > @@ -50,6 +51,11 @@ public:
> > >               exposureCompensation_ = gain;
> > >       }
> > >
> > > +     void setLux(unsigned int lux)
> > > +     {
> > > +             lux_ = lux;
> > > +     }
> > > +
> > >       void setLimits(utils::Duration minExposureTime, utils::Duration maxExposureTime,
> > >                      double minGain, double maxGain, std::vector<AgcConstraint> constraints);
> > >
> > > @@ -82,7 +88,7 @@ public:
> > >  private:
> > >       virtual double estimateLuminance(const double gain) const = 0;
> > >
> > > -     void parseRelativeLuminanceTarget(const YamlObject &tuningData);
> > > +     int parseRelativeLuminanceTarget(const YamlObject &tuningData);
> > >       void parseConstraint(const YamlObject &modeDict, int32_t id);
> > >       int parseConstraintModes(const YamlObject &tuningData);
> > >       int parseExposureModes(const YamlObject &tuningData);
> > > @@ -92,10 +98,12 @@ private:
> > >                                  double gain);
> > >       utils::Duration filterExposure(utils::Duration exposureValue);
> > >
> > > +     utils::Duration filteredExposure_;
> > > +     mutable bool luxWarningEnabled_;
> > >       double exposureCompensation_;
> > > +     Pwl relativeLuminanceTarget_;
> > >       uint64_t frameCount_;
> > > -     utils::Duration filteredExposure_;
> > > -     double relativeLuminanceTarget_;
> > > +     unsigned int lux_;
> > >
> > >       std::vector<AgcConstraint> additionalConstraints_;
> > >       std::map<int32_t, std::vector<AgcConstraint>> constraintModes_;
> > > diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp
> > > index f5a3c917cb69..1ecaff680978 100644
> > > --- a/src/ipa/rkisp1/algorithms/agc.cpp
> > > +++ b/src/ipa/rkisp1/algorithms/agc.cpp
> > > @@ -618,6 +618,7 @@ void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
> > >               effectiveExposureValue *= frameContext.agc.quantizationGain;
> > >
> > >       setExposureCompensation(pow(2.0, frameContext.agc.exposureValue));
> > > +     setLux(frameContext.lux.lux);
> > >
> > >       utils::Duration newExposureTime;
> > >       double aGain, qGain, dGain;
> > > --
> > > 2.51.0
> > >
Stefan Klug Nov. 21, 2025, 10:41 a.m. UTC | #6
Hi Jacopo,

Quoting Jacopo Mondi (2025-11-21 10:04:58)
> Hi Stefan
> 
> On Thu, Nov 20, 2025 at 06:54:28PM +0100, Stefan Klug wrote:
> > Hi Jacopo,
> >
> > Quoting Jacopo Mondi (2025-11-20 18:05:41)
> > > Hi Stefan
> > >
> > > On Wed, Nov 19, 2025 at 02:22:12PM +0100, Stefan Klug wrote:
> > > > In some situations it is necessary to specify the target brightness
> > > > value depending on the overall lux level. Replace the float
> > > > relativeLuminanceTraget by a PWL. As the PWL loading code loads a plain
> > > > value as single point PWL, backwards compatibility to existing tuning
> > > > files is ensured.
> > > >
> > > > While at it, order the class members in reverse xmas tree notation.
> > > >
> > > > Signed-off-by: Paul Elder <paul.elder@ideasonboard.com>
> > > > Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>
> > > > Reviewed-by: Daniel Scally <dan.scally@ideasonboard.com>
> > > >
> > > > ---
> > > >
> > > > Changes in v3:
> > > > - Dropped reference to original patch from commit message. For the
> > > >   record, the old patch was https://patchwork.libcamera.org/patch/20231/
> > > > - Ordered member variables in reverse xmas tree order
> > > > - Fallback to kDefaultRelativeLuminanceTarget if relativeLuminanceTraget
> > > >   is missing in the tuning file
> > > > - Warn once if lux level is not available after a few frames
> > > >
> > > > Changes in v2:
> > > > - Collected tags
> > > > - Dropped special handling of plain values, as this is now part of the
> > > >   yaml PWL loader.
> > > > ---
> > > >  src/ipa/libipa/agc_mean_luminance.cpp | 67 ++++++++++++++++++++++++---
> > > >  src/ipa/libipa/agc_mean_luminance.h   | 14 ++++--
> > > >  src/ipa/rkisp1/algorithms/agc.cpp     |  1 +
> > > >  3 files changed, 72 insertions(+), 10 deletions(-)
> > > >
> > > > diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp
> > > > index 64f36bd75dd2..4f2b5fad2082 100644
> > > > --- a/src/ipa/libipa/agc_mean_luminance.cpp
> > > > +++ b/src/ipa/libipa/agc_mean_luminance.cpp
> > > > @@ -54,6 +54,14 @@ static constexpr double kDefaultRelativeLuminanceTarget = 0.16;
> > > >   */
> > > >  static constexpr double kMaxRelativeLuminanceTarget = 0.95;
> > > >
> > > > +/*
> > > > + * Default lux level
> > > > + *
> > > > + * If no lux level or a zero lux level is specified, but PWLs are used to
> > > > + * specify luminance targets, this default level is used.
> > > > + */
> > > > +static constexpr unsigned int kDefaultLuxLevel = 500;
> > > > +
> > > >  /**
> > > >   * \struct AgcMeanLuminance::AgcConstraint
> > > >   * \brief The boundaries and target for an AeConstraintMode constraint
> > > > @@ -144,17 +152,30 @@ static constexpr double kMaxRelativeLuminanceTarget = 0.95;
> > > >   */
> > > >
> > > >  AgcMeanLuminance::AgcMeanLuminance()
> > > > -     : exposureCompensation_(1.0), frameCount_(0), filteredExposure_(0s),
> > > > -       relativeLuminanceTarget_(0)
> > > > +     : filteredExposure_(0s), luxWarningEnabled_(true),
> > > > +       exposureCompensation_(1.0), frameCount_(0), lux_(0)
> > > >  {
> > > >  }
> > > >
> > > >  AgcMeanLuminance::~AgcMeanLuminance() = default;
> > > >
> > > > -void AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)
> > > > +int AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)
> > > >  {
> > > > -     relativeLuminanceTarget_ =
> > > > -             tuningData["relativeLuminanceTarget"].get<double>(kDefaultRelativeLuminanceTarget);
> > > > +     auto &target = tuningData["relativeLuminanceTarget"];
> > > > +     if (!target) {
> > > > +             relativeLuminanceTarget_ = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } };
> > > > +             return 0;
> > > > +     }
> > > > +
> > > > +     auto pwl = target.get<Pwl>();
> > > > +     if (!pwl) {
> > > > +             LOG(AgcMeanLuminance, Error)
> > > > +                     << "Failed to load relative luminance target.";
> > > > +             return -EINVAL;
> > > > +     }
> > > > +
> > > > +     relativeLuminanceTarget_ = std::move(*pwl);
> > > > +     return 0;
> > > >  }
> > > >
> > > >  void AgcMeanLuminance::parseConstraint(const YamlObject &modeDict, int32_t id)
> > > > @@ -325,6 +346,8 @@ void AgcMeanLuminance::configure(utils::Duration lineDuration,
> > > >  {
> > > >       for (auto &[id, helper] : exposureModeHelpers_)
> > > >               helper->configure(lineDuration, sensorHelper);
> > > > +
> > > > +     luxWarningEnabled_ = true;
> > > >  }
> > > >
> > > >  /**
> > > > @@ -385,7 +408,9 @@ int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)
> > > >  {
> > > >       int ret;
> > > >
> > > > -     parseRelativeLuminanceTarget(tuningData);
> > > > +     ret = parseRelativeLuminanceTarget(tuningData);
> > > > +     if (ret)
> > > > +             return ret;
> > > >
> > > >       ret = parseConstraintModes(tuningData);
> > > >       if (ret)
> > > > @@ -403,6 +428,16 @@ int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)
> > > >   * AGC calculations. It is expressed as gain instead of EV.
> > > >   */
> > > >
> > > > +/**
> > > > + * \fn AgcMeanLuminance::setLux(int lux)
> > > > + * \brief Set the lux level
> > > > + * \param[in] lux The lux level
> > > > + *
> > > > + * This function sets the lux level to be used in the AGC calculations. A value
> > > > + * of 0 means no measurement and a default value of \a kDefaultLuxLevel is used
> > > > + * if necessary.
> > > > + */
> > > > +
> > > >  /**
> > > >   * \brief Set the ExposureModeHelper limits for this class
> > > >   * \param[in] minExposureTime Minimum exposure time to allow
> > > > @@ -538,7 +573,25 @@ double AgcMeanLuminance::constraintClampGain(uint32_t constraintModeIndex,
> > > >   */
> > > >  double AgcMeanLuminance::effectiveYTarget() const
> > > >  {
> > > > -     return std::min(relativeLuminanceTarget_ * exposureCompensation_,
> > > > +     double lux = lux_;
> > > > +     if (relativeLuminanceTarget_.size() > 1 && lux_ == 0) {
> > > > +             if (frameCount_ > 10 && luxWarningEnabled_) {
> > >
> > > My intention here was to push for all the messages related to parsing
> > > errors to be emitted once at startup, but I understand this is not
> > > possible here as the error condition depends on two things:
> > >
> > > 1) relativeLuminanceTarget_ is specified as a PWL which associates a
> > > lux level to a desired Y target
> > > 2) the IPA doesn't call setLux()
> > >
> > > Now, while 1) is relatively easy to spot at parsing time 2) only
> > > depend on the IPA implementation.
> > >
> > > AgcMeanLuminance::effectiveYTarget() is called in 2 places by IPAs:
> > >
> > > 1) configure() where I guess it's ok not to have a lux level set yet ?
> > > 2) process() where it is expected to be always called after setLux() ?
> > >
> > > if the above are true then I understand we should not Warn on
> > > configure but we should Warn on the very first frame goes through
> > > process for which effectiveYTarget() is called without a setLux() before?
> > >
> > > I'm not sure I get why 10 is used, if that's the case.
> >
> > 10 was arbitrarily chosen, so that we get at least something.
> > Reading your message I realize, that I was a few steps further and
> > already planning for the regulation rework which makes this hard to
> > follow.
> >
> > You are right, currently it is called within process() where the agc
> > calculation happens. The issue here is that this is plain wrong but
> > fixing needs to wait until after the regulation series.
> >
> > The agc calculation and also setLux() should happen in prepare(). Reason
> > is that for example frameContext.agc.exposureValue feeds into the agc
> > calculations. So we get stats for frame n, calculate new exposure
> > settings based on fc[n].agc.exposureValue and these will then be used in
> > prepare(n+lookahead). That is wrong.
> >
> > The correct thing to do would be to cache the stats but do the agc
> > calculations in prepare() so that fc[n+lookahed].agc.exposureValue feeds
> > into the calculation.
> >
> > When the above is fixed, there will be no lux value available for the
> > first few frames as prepare() is run for these frames before the stats
> > for frame 0 have arrived which is the time when the first lux value can
> > be calculated.
> 
> Couldn't we then avoid all error condition by:
> 
> - coalesce setLux() in effectiveYTarget() as Barnabas suggested by
>   making the Lux value an (optional<>?) parameter
> - use a default Lux value in the caller until we don't get a real lux
>   value OR pass nullopt

We can do that but then the (as we see not trivial) error logging would
also be on the caller side. The warning was intended for cases when:
- The user intended to use a lux dependent yTarget by providing the PWL
  in the tuning file
- And the user did not put the lux algorithm before the Agc, so that lux
  won't be set.

In this case the user would most likely not notice that anything is
wrong as these detailed regulations are difficult to test. So a warning
might help.

> 
> This will make mandatory for the IPA to pass a lux in. Is it something
> that you would expect ?

The reason for not making them parameters but passing them in via a side
channel was to not force us to implement lux on all platforms at the
same time (or splatter todos all over the place). Same for exposure
value.

Another option might be to pass in a configuration struct instead of
single parameters, so that we don't have to touch every IPA, when we
add additional features.

I don't think we should mandate a lux value. I'd be fine with a
std::optional but then imho in a config struct. But maybe on a separate
series?

Best regards,
Stefan

> 
> >
> > 10 is bigger than the number of frames that get prepared in advance and
> > is small enough to show up early on.
> >
> > Does that make sense?
> >
> > Best regards,
> > Stefan
> >
> > >
> > > Thanks
> > >   j
> > >
> > > > +                     luxWarningEnabled_ = false;
> > > > +                     LOG(AgcMeanLuminance, Warning)
> > > > +                             << "Missing lux value for luminance target "
> > > > +                                "calculation, default to "
> > > > +                             << kDefaultLuxLevel
> > > > +                             << ". Note that the Lux algorithm must be "
> > > > +                                "included before the Agc algorithm.";
> > > > +             }
> > > > +
> > > > +             lux = kDefaultLuxLevel;
> > > > +     }
> > > > +
> > > > +     double luminanceTarget = relativeLuminanceTarget_.eval(
> > > > +             relativeLuminanceTarget_.domain().clamp(lux));
> > > > +
> > > > +     return std::min(luminanceTarget * exposureCompensation_,
> > > >                       kMaxRelativeLuminanceTarget);
> > > >  }
> > > >
> > > > diff --git a/src/ipa/libipa/agc_mean_luminance.h b/src/ipa/libipa/agc_mean_luminance.h
> > > > index e5f164c3186b..746b97b16ffe 100644
> > > > --- a/src/ipa/libipa/agc_mean_luminance.h
> > > > +++ b/src/ipa/libipa/agc_mean_luminance.h
> > > > @@ -20,6 +20,7 @@
> > > >
> > > >  #include "exposure_mode_helper.h"
> > > >  #include "histogram.h"
> > > > +#include "pwl.h"
> > > >
> > > >  namespace libcamera {
> > > >
> > > > @@ -50,6 +51,11 @@ public:
> > > >               exposureCompensation_ = gain;
> > > >       }
> > > >
> > > > +     void setLux(unsigned int lux)
> > > > +     {
> > > > +             lux_ = lux;
> > > > +     }
> > > > +
> > > >       void setLimits(utils::Duration minExposureTime, utils::Duration maxExposureTime,
> > > >                      double minGain, double maxGain, std::vector<AgcConstraint> constraints);
> > > >
> > > > @@ -82,7 +88,7 @@ public:
> > > >  private:
> > > >       virtual double estimateLuminance(const double gain) const = 0;
> > > >
> > > > -     void parseRelativeLuminanceTarget(const YamlObject &tuningData);
> > > > +     int parseRelativeLuminanceTarget(const YamlObject &tuningData);
> > > >       void parseConstraint(const YamlObject &modeDict, int32_t id);
> > > >       int parseConstraintModes(const YamlObject &tuningData);
> > > >       int parseExposureModes(const YamlObject &tuningData);
> > > > @@ -92,10 +98,12 @@ private:
> > > >                                  double gain);
> > > >       utils::Duration filterExposure(utils::Duration exposureValue);
> > > >
> > > > +     utils::Duration filteredExposure_;
> > > > +     mutable bool luxWarningEnabled_;
> > > >       double exposureCompensation_;
> > > > +     Pwl relativeLuminanceTarget_;
> > > >       uint64_t frameCount_;
> > > > -     utils::Duration filteredExposure_;
> > > > -     double relativeLuminanceTarget_;
> > > > +     unsigned int lux_;
> > > >
> > > >       std::vector<AgcConstraint> additionalConstraints_;
> > > >       std::map<int32_t, std::vector<AgcConstraint>> constraintModes_;
> > > > diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp
> > > > index f5a3c917cb69..1ecaff680978 100644
> > > > --- a/src/ipa/rkisp1/algorithms/agc.cpp
> > > > +++ b/src/ipa/rkisp1/algorithms/agc.cpp
> > > > @@ -618,6 +618,7 @@ void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
> > > >               effectiveExposureValue *= frameContext.agc.quantizationGain;
> > > >
> > > >       setExposureCompensation(pow(2.0, frameContext.agc.exposureValue));
> > > > +     setLux(frameContext.lux.lux);
> > > >
> > > >       utils::Duration newExposureTime;
> > > >       double aGain, qGain, dGain;
> > > > --
> > > > 2.51.0
> > > >

Patch
diff mbox series

diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp
index 64f36bd75dd2..4f2b5fad2082 100644
--- a/src/ipa/libipa/agc_mean_luminance.cpp
+++ b/src/ipa/libipa/agc_mean_luminance.cpp
@@ -54,6 +54,14 @@  static constexpr double kDefaultRelativeLuminanceTarget = 0.16;
  */
 static constexpr double kMaxRelativeLuminanceTarget = 0.95;
 
+/*
+ * Default lux level
+ *
+ * If no lux level or a zero lux level is specified, but PWLs are used to
+ * specify luminance targets, this default level is used.
+ */
+static constexpr unsigned int kDefaultLuxLevel = 500;
+
 /**
  * \struct AgcMeanLuminance::AgcConstraint
  * \brief The boundaries and target for an AeConstraintMode constraint
@@ -144,17 +152,30 @@  static constexpr double kMaxRelativeLuminanceTarget = 0.95;
  */
 
 AgcMeanLuminance::AgcMeanLuminance()
-	: exposureCompensation_(1.0), frameCount_(0), filteredExposure_(0s),
-	  relativeLuminanceTarget_(0)
+	: filteredExposure_(0s), luxWarningEnabled_(true),
+	  exposureCompensation_(1.0), frameCount_(0), lux_(0)
 {
 }
 
 AgcMeanLuminance::~AgcMeanLuminance() = default;
 
-void AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)
+int AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)
 {
-	relativeLuminanceTarget_ =
-		tuningData["relativeLuminanceTarget"].get<double>(kDefaultRelativeLuminanceTarget);
+	auto &target = tuningData["relativeLuminanceTarget"];
+	if (!target) {
+		relativeLuminanceTarget_ = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } };
+		return 0;
+	}
+
+	auto pwl = target.get<Pwl>();
+	if (!pwl) {
+		LOG(AgcMeanLuminance, Error)
+			<< "Failed to load relative luminance target.";
+		return -EINVAL;
+	}
+
+	relativeLuminanceTarget_ = std::move(*pwl);
+	return 0;
 }
 
 void AgcMeanLuminance::parseConstraint(const YamlObject &modeDict, int32_t id)
@@ -325,6 +346,8 @@  void AgcMeanLuminance::configure(utils::Duration lineDuration,
 {
 	for (auto &[id, helper] : exposureModeHelpers_)
 		helper->configure(lineDuration, sensorHelper);
+
+	luxWarningEnabled_ = true;
 }
 
 /**
@@ -385,7 +408,9 @@  int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)
 {
 	int ret;
 
-	parseRelativeLuminanceTarget(tuningData);
+	ret = parseRelativeLuminanceTarget(tuningData);
+	if (ret)
+		return ret;
 
 	ret = parseConstraintModes(tuningData);
 	if (ret)
@@ -403,6 +428,16 @@  int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)
  * AGC calculations. It is expressed as gain instead of EV.
  */
 
+/**
+ * \fn AgcMeanLuminance::setLux(int lux)
+ * \brief Set the lux level
+ * \param[in] lux The lux level
+ *
+ * This function sets the lux level to be used in the AGC calculations. A value
+ * of 0 means no measurement and a default value of \a kDefaultLuxLevel is used
+ * if necessary.
+ */
+
 /**
  * \brief Set the ExposureModeHelper limits for this class
  * \param[in] minExposureTime Minimum exposure time to allow
@@ -538,7 +573,25 @@  double AgcMeanLuminance::constraintClampGain(uint32_t constraintModeIndex,
  */
 double AgcMeanLuminance::effectiveYTarget() const
 {
-	return std::min(relativeLuminanceTarget_ * exposureCompensation_,
+	double lux = lux_;
+	if (relativeLuminanceTarget_.size() > 1 && lux_ == 0) {
+		if (frameCount_ > 10 && luxWarningEnabled_) {
+			luxWarningEnabled_ = false;
+			LOG(AgcMeanLuminance, Warning)
+				<< "Missing lux value for luminance target "
+				   "calculation, default to "
+				<< kDefaultLuxLevel
+				<< ". Note that the Lux algorithm must be "
+				   "included before the Agc algorithm.";
+		}
+
+		lux = kDefaultLuxLevel;
+	}
+
+	double luminanceTarget = relativeLuminanceTarget_.eval(
+		relativeLuminanceTarget_.domain().clamp(lux));
+
+	return std::min(luminanceTarget * exposureCompensation_,
 			kMaxRelativeLuminanceTarget);
 }
 
diff --git a/src/ipa/libipa/agc_mean_luminance.h b/src/ipa/libipa/agc_mean_luminance.h
index e5f164c3186b..746b97b16ffe 100644
--- a/src/ipa/libipa/agc_mean_luminance.h
+++ b/src/ipa/libipa/agc_mean_luminance.h
@@ -20,6 +20,7 @@ 
 
 #include "exposure_mode_helper.h"
 #include "histogram.h"
+#include "pwl.h"
 
 namespace libcamera {
 
@@ -50,6 +51,11 @@  public:
 		exposureCompensation_ = gain;
 	}
 
+	void setLux(unsigned int lux)
+	{
+		lux_ = lux;
+	}
+
 	void setLimits(utils::Duration minExposureTime, utils::Duration maxExposureTime,
 		       double minGain, double maxGain, std::vector<AgcConstraint> constraints);
 
@@ -82,7 +88,7 @@  public:
 private:
 	virtual double estimateLuminance(const double gain) const = 0;
 
-	void parseRelativeLuminanceTarget(const YamlObject &tuningData);
+	int parseRelativeLuminanceTarget(const YamlObject &tuningData);
 	void parseConstraint(const YamlObject &modeDict, int32_t id);
 	int parseConstraintModes(const YamlObject &tuningData);
 	int parseExposureModes(const YamlObject &tuningData);
@@ -92,10 +98,12 @@  private:
 				   double gain);
 	utils::Duration filterExposure(utils::Duration exposureValue);
 
+	utils::Duration filteredExposure_;
+	mutable bool luxWarningEnabled_;
 	double exposureCompensation_;
+	Pwl relativeLuminanceTarget_;
 	uint64_t frameCount_;
-	utils::Duration filteredExposure_;
-	double relativeLuminanceTarget_;
+	unsigned int lux_;
 
 	std::vector<AgcConstraint> additionalConstraints_;
 	std::map<int32_t, std::vector<AgcConstraint>> constraintModes_;
diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp
index f5a3c917cb69..1ecaff680978 100644
--- a/src/ipa/rkisp1/algorithms/agc.cpp
+++ b/src/ipa/rkisp1/algorithms/agc.cpp
@@ -618,6 +618,7 @@  void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
 		effectiveExposureValue *= frameContext.agc.quantizationGain;
 
 	setExposureCompensation(pow(2.0, frameContext.agc.exposureValue));
+	setLux(frameContext.lux.lux);
 
 	utils::Duration newExposureTime;
 	double aGain, qGain, dGain;