[v5,06/24] ipa: libipa: agc: Move tuning file parsing
diff mbox series

Message ID 20261029125844.170324-7-barnabas.pocze@ideasonboard.com
State New
Headers show
Series
  • ipa: libipa: agc: Take exposure margin into account
Related show

Commit Message

Barnabás Pőcze Oct. 29, 2026, 12:58 p.m. UTC
Move the tuning file parsing out of `AgcMeanLuminance`, and provide the
configuration via the constructor. This allows `AgcAlgorithm` to inspect
the configuration, specifically it can check if any exposure modes have
been defined.

Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
---
 src/ipa/libipa/agc.cpp                | 254 +++++++++++++++++++++++-
 src/ipa/libipa/agc_mean_luminance.cpp | 268 ++------------------------
 src/ipa/libipa/agc_mean_luminance.h   |  22 +--
 3 files changed, 275 insertions(+), 269 deletions(-)

Comments

Jacopo Mondi Oct. 29, 2026, 3:40 p.m. UTC | #1
Hi Barnabás

On Thu, Oct 29, 2026 at 01:58:26PM +0100, Barnabás Pőcze wrote:
> Move the tuning file parsing out of `AgcMeanLuminance`, and provide the
> configuration via the constructor. This allows `AgcAlgorithm` to inspect
> the configuration, specifically it can check if any exposure modes have
> been defined.
>
> Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
> ---
>  src/ipa/libipa/agc.cpp                | 254 +++++++++++++++++++++++-
>  src/ipa/libipa/agc_mean_luminance.cpp | 268 ++------------------------
>  src/ipa/libipa/agc_mean_luminance.h   |  22 +--
>  3 files changed, 275 insertions(+), 269 deletions(-)
>
> diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
> index 7438825d30..54aa8207d4 100644
> --- a/src/ipa/libipa/agc.cpp
> +++ b/src/ipa/libipa/agc.cpp
> @@ -26,6 +26,8 @@
>
>  #include <libcamera/ipa/core_ipa_interface.h>
>
> +#include "exposure_mode_helper.h"
> +
>  /**
>   * \file agc.h
>   * \brief libipa AGC algorithm
> @@ -222,6 +224,196 @@ namespace agc {
>
>  } /* namespace agc */
>
> +namespace {
> +
> +/*
> + * Default relative luminance target
> + *
> + * This value should be chosen so that when the camera points at a grey target,
> + * the resulting image brightness looks "right". Custom values can be passed
> + * as the relativeLuminanceTarget value in sensor tuning files.
> + */
> +static constexpr double kDefaultRelativeLuminanceTarget = 0.16;
> +
> +int parseRelativeLuminanceTarget(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData)
> +{
> +	auto &target = tuningData["relativeLuminanceTarget"];
> +	if (!target) {
> +		config.relativeLuminanceTarget = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } };
> +		return 0;
> +	}
> +
> +	auto pwl = target.get<Pwl>();
> +	if (!pwl) {
> +		LOG(Agc, Error)
> +			<< "Failed to load relative luminance target.";
> +		return -EINVAL;
> +	}
> +
> +	config.relativeLuminanceTarget = std::move(*pwl);
> +	return 0;
> +}
> +
> +int parseConstraint(AgcMeanLuminance::Configuration &config, const ValueNode &modeDict, int32_t id)
> +{
> +	for (const auto &[boundName, content] : modeDict.asDict()) {
> +		if (boundName != "upper" && boundName != "lower") {
> +			LOG(Agc, Warning)
> +				<< "Ignoring unknown constraint bound '" << boundName << "'";
> +			continue;
> +		}
> +
> +		unsigned int idx = static_cast<unsigned int>(boundName == "upper");
> +		auto bound = static_cast<AgcMeanLuminance::AgcConstraint::Bound>(idx);
> +		double qLo = content["qLo"].get<double>().value_or(0.98);
> +		double qHi = content["qHi"].get<double>().value_or(1.0);
> +		auto yTarget = content["yTarget"].get<Pwl>();
> +		if (!yTarget) {
> +			LOG(Agc, Error)
> +				<< "Failed to parse yTarget";
> +			return -EINVAL;
> +		}
> +
> +		AgcMeanLuminance::AgcConstraint constraint = {
> +			bound,
> +			qLo,
> +			qHi,
> +			std::move(*yTarget),
> +		};
> +		auto &constraints = config.constraintModes[id];
> +
> +		if (idx)
> +			constraints.push_back(std::move(constraint));
> +		else
> +			constraints.insert(constraints.begin(), std::move(constraint));
> +	}
> +
> +	return 0;
> +}
> +
> +int parseConstraintModes(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData)
> +{
> +	const ValueNode &constraintModes = tuningData[controls::AeConstraintMode.name()];
> +	if (constraintModes.isDictionary()) {
> +		for (const auto &[modeName, modeDict] : constraintModes.asDict()) {
> +			auto it = controls::AeConstraintModeNameValueMap.find(modeName);
> +			if (it == controls::AeConstraintModeNameValueMap.end()) {
> +				LOG(Agc, Warning)
> +					<< "Skipping unknown constraint mode '" << modeName << "'";
> +				continue;
> +			}
> +
> +			if (!modeDict.isDictionary()) {
> +				LOG(Agc, Error)
> +					<< "Invalid constraint mode '" << modeName << "'";
> +				return -EINVAL;
> +			}
> +
> +			int ret = parseConstraint(config, modeDict, it->second);
> +			if (ret)
> +				return ret;
> +		}
> +	}
> +
> +	/*
> +	 * If the tuning data file contains no constraints then we use the
> +	 * default constraint that the IPU3/RkISP1 Agc algorithms were adhering
> +	 * to anyway before centralisation; this constraint forces the top 2% of
> +	 * the histogram to be at least 0.5.
> +	 */
> +	if (config.constraintModes.empty()) {
> +		AgcMeanLuminance::AgcConstraint constraint = {
> +			AgcMeanLuminance::AgcConstraint::Bound::Lower,
> +			0.98,
> +			1.0,
> +			Pwl({ { { 0.0, 0.5 } } }),
> +		};
> +
> +		config.constraintModes[controls::AeConstraintModeNormal].push_back(
> +			std::move(constraint));
> +	}
> +
> +	return 0;
> +}
> +
> +int parseExposureModes(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData)
> +{
> +	const ValueNode &exposureModes = tuningData[controls::AeExposureMode.name()];
> +	if (exposureModes.isDictionary()) {
> +		for (const auto &[modeName, modeValues] : exposureModes.asDict()) {
> +			auto it = controls::AeExposureModeNameValueMap.find(modeName);
> +			if (it == controls::AeExposureModeNameValueMap.end()) {
> +				LOG(Agc, Warning)
> +					<< "Skipping unknown exposure mode '" << modeName << "'";
> +				continue;
> +			}
> +
> +			if (!modeValues.isDictionary()) {
> +				LOG(Agc, Error)
> +					<< "Invalid exposure mode '" << modeName << "'";
> +				return -EINVAL;
> +			}
> +
> +			std::vector<uint32_t> exposureTimes =
> +				modeValues["exposureTime"].get<std::vector<uint32_t>>().value_or(utils::defopt);
> +			std::vector<double> gains =
> +				modeValues["gain"].get<std::vector<double>>().value_or(utils::defopt);
> +
> +			if (exposureTimes.size() != gains.size()) {
> +				LOG(Agc, Error)
> +					<< "Exposure time and gain array sizes unequal";
> +				return -EINVAL;
> +			}
> +
> +			if (exposureTimes.empty()) {
> +				LOG(Agc, Error)
> +					<< "Exposure time and gain arrays are empty";
> +				return -EINVAL;
> +			}
> +
> +			std::vector<ExposureModeHelper::Stage> stages;
> +			for (size_t i = 0; i < exposureTimes.size(); i++) {
> +				stages.push_back({
> +					.exposureTime = std::chrono::microseconds(exposureTimes[i]),
> +					.gain = gains[i],
> +				});
> +			}
> +
> +			config.exposureModeHelpers.try_emplace(it->second, std::move(stages));
> +		}
> +	}
> +
> +	/*
> +	 * If we don't have any exposure modes in the tuning data we create an
> +	 * ExposureModeHelper using an empty vector of stages. This will result
> +	 * in the ExposureModeHelper simply driving the exposure time as high as
> +	 * possible before touching gain.
> +	 */
> +	if (config.exposureModeHelpers.empty())
> +		config.exposureModeHelpers.try_emplace(
> +			controls::AeExposureModeNormal,
> +			std::vector<ExposureModeHelper::Stage>{});
> +
> +	return 0;
> +}
> +
> +int parseTuningData(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData)
> +{
> +	int ret;
> +
> +	ret = parseRelativeLuminanceTarget(config, tuningData);
> +	if (ret)
> +		return ret;
> +
> +	ret = parseConstraintModes(config, tuningData);
> +	if (ret)
> +		return ret;
> +
> +	return parseExposureModes(config, tuningData);
> +}
> +
> +} /* namespace */
> +
>  /**
>   * \class AgcAlgorithm
>   * \brief libIPA LSC algorithm algorithm
> @@ -238,6 +430,57 @@ namespace agc {
>   * virtual function, they should simply call the identically named member
>   * function of AgcAlgorithm.
>   *
> + * When a CameraSensorHelper is provided, AgcMeanLuminance is used and support for
> + * exposure (controls::AeExposureMode) and constraint (controls::AeConstraintMode)
> + * modes, and specifying the default luminance target is available via the tuning data.
> + *
> + * For controls::AeConstraintMode, the dictionary under the key "AeConstraintMode"
> + * is parsed: per-mode setting dictionaries with the key being a value from
> + * controls::AeConstraintModeNameValueMap. The yTarget can either be provided
> + * as single value or as array in which case it is interpreted as a PWL mapping
> + * lux levels to yTarget values. Each mode dict may contain either a "lower" or
> + * "upper" key or both, for example:
> + *
> + * \code{.unparsed}
> + * algorithms:
> + *   - Agc:
> + *       AeConstraintMode:
> + *         Normal:
> + *           lower:
> + *             qLo: 0.98
> + *             qHi: 1.0
> + *             yTarget: 0.5
> + *         Highlight:
> + *           lower:
> + *             qLo: 0.98
> + *             qHi: 1.0
> + *             yTarget: 0.5
> + *           upper:
> + *             qLo: 0.98
> + *             qHi: 1.0
> + *             yTarget: [ 100, 0.8, 20000, 0.5 ]
> + *
> + * \endcode
> + *
> + * For controls::AeExposureMode, the dictionary under the key AeExposureMode
> + * is parsed: per-mode setting dictionaries with the key being a value from
> + * controls::AeExposureModeNameValueMap. Each mode dict should contain
> + * an array of exposure times with the key "exposureTime" and an array of gain
> + * values with the key "gain", in this format:
> + *
> + * \code{.unparsed}
> + * algorithms:
> + *   - Agc:
> + *       AeExposureMode:
> + *         Normal:
> + *           exposureTime: [ 100, 10000, 30000, 60000, 120000 ]
> + *           gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ]
> + *         Short:
> + *           exposureTime: [ 100, 10000, 30000, 60000, 120000 ]
> + *           gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ]
> + *
> + * \endcode
> + *
>   * \todo DigitalGain, DigitalGainMode
>   * \todo Expand documentation
>   */
> @@ -298,9 +541,8 @@ namespace agc {
>   * in ConfigurationParams::ctrlMap.
>   *
>   * If a CameraSensorHelper is provided in \a sensor, an instance of
> - * AgcMeanLuminance is instantiated and the tuning data is loaded by
> - * calling AgcMeanLuminance::parseTuningData(). Otherwise an instance
> - * of AgcMSV is instantiated, and in this case the tuning data is ignored.
> + * AgcMeanLuminance is instantiated. Otherwise an instance of AgcMSV
> + * is instantiated, and in this case the tuning data is ignored.
>   *
>   * \return 0 on success, or a negative error code
>   *
> @@ -310,10 +552,12 @@ int AgcAlgorithm::init(const ValueNode &tuningData, CameraSensorHelper *sensor,
>  		       const ConfigurationParams &config)
>  {
>  	if (sensor) {
> -		auto &impl = impl_.emplace<AgcMeanLuminance>();
> -		int ret = impl.parseTuningData(tuningData);
> +		AgcMeanLuminance::Configuration agcConfig;
> +		int ret = parseTuningData(agcConfig, tuningData);
>  		if (ret)
>  			return ret;
> +
> +		impl_.emplace<AgcMeanLuminance>(std::move(agcConfig));
>  	} else {
>  		impl_.emplace<AgcMSV>();
>  	}
> diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp
> index 4c47770424..4f2b5edef6 100644
> --- a/src/ipa/libipa/agc_mean_luminance.cpp
> +++ b/src/ipa/libipa/agc_mean_luminance.cpp
> @@ -9,7 +9,6 @@
>
>  #include <algorithm>
>  #include <cmath>
> -#include <span>
>
>  #include <libcamera/base/log.h>
>  #include <libcamera/base/utils.h>
> @@ -18,8 +17,6 @@
>
>  #include "exposure_mode_helper.h"
>
> -using namespace libcamera::controls;
> -
>  /**
>   * \file agc_mean_luminance.h
>   * \brief Class implementing mean luminance AEGC
> @@ -39,15 +36,6 @@ namespace ipa {
>   */
>  static constexpr uint32_t kNumStartupFrames = 10;
>
> -/*
> - * Default relative luminance target
> - *
> - * This value should be chosen so that when the camera points at a grey target,
> - * the resulting image brightness looks "right". Custom values can be passed
> - * as the relativeLuminanceTarget value in sensor tuning files.
> - */
> -static constexpr double kDefaultRelativeLuminanceTarget = 0.16;
> -
>  /*
>   * Maximum relative luminance target
>   *
> @@ -148,17 +136,13 @@ static constexpr unsigned int kDefaultLuxLevel = 500;
>   * section of a histogram to a target value, where the target and the boundaries
>   * of the section of the histogram used in the calculation are taken from the
>   * values defined for the currently configured AeConstraintMode within the
> - * tuning data. This class provides a helper function to parse those tuning data
> - * to discover the constraints, and so requires a specific format for those
> - * data which is described in \ref parseTuningData(). The gain from the first
> - * stage is then clamped to the gain from this stage.
> + * tuning data. The gain from the first stage is then clamped to the gain from
> + * this stage.
>   *
>   * The final gain is used to adjust the effective exposure value of the image,
>   * and that new exposure value is divided into exposure time, analogue gain and
>   * digital gain according to the selected AeExposureMode. This class uses the
> - * \ref ExposureModeHelper class to assist in that division, and expects the
> - * data needed to initialise that class to be present in tuning data in a
> - * format described in \ref parseTuningData().
> + * \ref ExposureModeHelper class to assist in that division.
>   *
>   * In order to be able to use this algorithm an IPA module needs to be able to
>   * do the following:
> @@ -170,175 +154,23 @@ static constexpr unsigned int kDefaultLuxLevel = 500;
>   *
>   * IPA modules that want to use this class to implement their AEGC algorithm
>   * should derive AgcMeanLuminance::Traits and override the necessary functions.
> - * The users must call parseTuningData() in init(), and must also call configure()
> - * and setLimits() in configure(). They may then use calculateNewEv() in process().
> - * If the limits passed to setLimits() change for any reason (for example, in
> - * response to a FrameDurationLimit control being passed in queueRequest()) then
> - * setLimits() must be called again with the new values.
> + * The users must call configure() and setLimits() in configure(). They may
> + * then use calculateNewEv() in process(). If the limits passed to setLimits()
> + * change for any reason (for example, in response to a FrameDurationLimit control
> + * being passed in queueRequest()) then setLimits() must be called again with the
> + * new values.
>   */
>
> -AgcMeanLuminance::AgcMeanLuminance()
> -	: filteredExposure_(0s), luxWarningEnabled_(true), frameCount_(0)
> +AgcMeanLuminance::AgcMeanLuminance(Configuration &&config)
> +	: filteredExposure_(0s), luxWarningEnabled_(true), frameCount_(0),
> +	  relativeLuminanceTarget_(std::move(config.relativeLuminanceTarget)),
> +	  constraintModes_(std::move(config.constraintModes)),
> +	  exposureModeHelpers_(std::move(config.exposureModeHelpers))
>  {
>  }
>
>  AgcMeanLuminance::~AgcMeanLuminance() = default;
>
> -int AgcMeanLuminance::parseRelativeLuminanceTarget(const ValueNode &tuningData)
> -{
> -	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;
> -}
> -
> -int AgcMeanLuminance::parseConstraint(const ValueNode &modeDict, int32_t id)
> -{
> -	for (const auto &[boundName, content] : modeDict.asDict()) {
> -		if (boundName != "upper" && boundName != "lower") {
> -			LOG(AgcMeanLuminance, Warning)
> -				<< "Ignoring unknown constraint bound '" << boundName << "'";
> -			continue;
> -		}
> -
> -		unsigned int idx = static_cast<unsigned int>(boundName == "upper");
> -		AgcConstraint::Bound bound = static_cast<AgcConstraint::Bound>(idx);
> -		double qLo = content["qLo"].get<double>().value_or(0.98);
> -		double qHi = content["qHi"].get<double>().value_or(1.0);
> -		auto yTarget = content["yTarget"].get<Pwl>();
> -		if (!yTarget) {
> -			LOG(AgcMeanLuminance, Error)
> -				<< "Failed to parse yTarget";
> -			return -EINVAL;
> -		}
> -
> -		AgcConstraint constraint = { bound, qLo, qHi, std::move(*yTarget) };
> -		auto &constraints = constraintModes_[id];
> -
> -		if (idx)
> -			constraints.push_back(std::move(constraint));
> -		else
> -			constraints.insert(constraints.begin(), std::move(constraint));
> -	}
> -
> -	return 0;
> -}
> -
> -int AgcMeanLuminance::parseConstraintModes(const ValueNode &tuningData)
> -{
> -	const ValueNode &constraintModes = tuningData[controls::AeConstraintMode.name()];
> -	if (constraintModes.isDictionary()) {
> -		for (const auto &[modeName, modeDict] : constraintModes.asDict()) {
> -			auto it = AeConstraintModeNameValueMap.find(modeName);
> -			if (it == AeConstraintModeNameValueMap.end()) {
> -				LOG(AgcMeanLuminance, Warning)
> -					<< "Skipping unknown constraint mode '" << modeName << "'";
> -				continue;
> -			}
> -
> -			if (!modeDict.isDictionary()) {
> -				LOG(AgcMeanLuminance, Error)
> -					<< "Invalid constraint mode '" << modeName << "'";
> -				return -EINVAL;
> -			}
> -
> -			int ret = parseConstraint(modeDict, it->second);
> -			if (ret)
> -				return ret;
> -		}
> -	}
> -
> -	/*
> -	 * If the tuning data file contains no constraints then we use the
> -	 * default constraint that the IPU3/RkISP1 Agc algorithms were adhering
> -	 * to anyway before centralisation; this constraint forces the top 2% of
> -	 * the histogram to be at least 0.5.
> -	 */
> -	if (constraintModes_.empty()) {
> -		AgcConstraint constraint = {
> -			AgcConstraint::Bound::Lower,
> -			0.98,
> -			1.0,
> -			Pwl({ { { 0.0, 0.5 } } })
> -		};
> -
> -		constraintModes_[controls::AeConstraintModeNormal].push_back(std::move(constraint));
> -	}
> -
> -	return 0;
> -}
> -
> -int AgcMeanLuminance::parseExposureModes(const ValueNode &tuningData)
> -{
> -	const ValueNode &exposureModes = tuningData[controls::AeExposureMode.name()];
> -	if (exposureModes.isDictionary()) {
> -		for (const auto &[modeName, modeValues] : exposureModes.asDict()) {
> -			auto it = AeExposureModeNameValueMap.find(modeName);
> -			if (it == AeExposureModeNameValueMap.end()) {
> -				LOG(AgcMeanLuminance, Warning)
> -					<< "Skipping unknown exposure mode '" << modeName << "'";
> -				continue;
> -			}
> -
> -			if (!modeValues.isDictionary()) {
> -				LOG(AgcMeanLuminance, Error)
> -					<< "Invalid exposure mode '" << modeName << "'";
> -				return -EINVAL;
> -			}
> -
> -			std::vector<uint32_t> exposureTimes =
> -				modeValues["exposureTime"].get<std::vector<uint32_t>>().value_or(utils::defopt);
> -			std::vector<double> gains =
> -				modeValues["gain"].get<std::vector<double>>().value_or(utils::defopt);
> -
> -			if (exposureTimes.size() != gains.size()) {
> -				LOG(AgcMeanLuminance, Error)
> -					<< "Exposure time and gain array sizes unequal";
> -				return -EINVAL;
> -			}
> -
> -			if (exposureTimes.empty()) {
> -				LOG(AgcMeanLuminance, Error)
> -					<< "Exposure time and gain arrays are empty";
> -				return -EINVAL;
> -			}
> -
> -			std::vector<ExposureModeHelper::Stage> stages;
> -			for (size_t i = 0; i < exposureTimes.size(); i++) {
> -				stages.push_back({
> -					.exposureTime = std::chrono::microseconds(exposureTimes[i]),
> -					.gain = gains[i],
> -				});
> -			}
> -
> -			exposureModeHelpers_.try_emplace(it->second, std::move(stages));
> -		}
> -	}
> -
> -	/*
> -	 * If we don't have any exposure modes in the tuning data we create an
> -	 * ExposureModeHelper using an empty vector of stages. This will result
> -	 * in the ExposureModeHelper simply driving the exposure time as high as
> -	 * possible before touching gain.
> -	 */
> -	if (exposureModeHelpers_.empty())
> -		exposureModeHelpers_.try_emplace(controls::AeExposureModeNormal,
> -						 std::vector<ExposureModeHelper::Stage>{});
> -
> -	return 0;
> -}
> -
>  /**
>   * \brief Configure the exposure mode helpers
>   * \param[in] lineDuration The sensor line length
> @@ -358,76 +190,6 @@ void AgcMeanLuminance::configure(utils::Duration lineDuration,
>  	frameCount_ = 0;
>  }
>
> -/**
> - * \brief Parse tuning data for AeConstraintMode and AeExposureMode controls
> - * \param[in] tuningData the ValueNode representing the tuning data
> - *
> - * This function parses tuning data to build the list of allowed values for the
> - * AeConstraintMode and AeExposureMode controls. Those tuning data must provide
> - * the data in a specific format; the Agc algorithm's tuning data should contain
> - * a dictionary called AeConstraintMode containing per-mode setting dictionaries
> - * with the key being a value from \ref controls::AeConstraintModeNameValueMap.
> - * The yTarget can either be provided as single value or as array in which case
> - * it is interpreted as a PWL mapping lux levels to yTarget values. Each mode
> - * dict may contain either a "lower" or "upper" key or both, for example:
> - *
> - * \code{.unparsed}
> - * algorithms:
> - *   - Agc:
> - *       AeConstraintMode:
> - *         Normal:
> - *           lower:
> - *             qLo: 0.98
> - *             qHi: 1.0
> - *             yTarget: 0.5
> - *         Highlight:
> - *           lower:
> - *             qLo: 0.98
> - *             qHi: 1.0
> - *             yTarget: 0.5
> - *           upper:
> - *             qLo: 0.98
> - *             qHi: 1.0
> - *             yTarget: [ 100, 0.8, 20000, 0.5 ]
> - *
> - * \endcode
> - *
> - * For the AeExposureMode control the data should contain a dictionary called
> - * AeExposureMode containing per-mode setting dictionaries with the key being a
> - * value from \ref controls::AeExposureModeNameValueMap. Each mode dict should
> - * contain an array of exposure times with the key "exposureTime" and an array
> - * of gain values with the key "gain", in this format:
> - *
> - * \code{.unparsed}
> - * algorithms:
> - *   - Agc:
> - *       AeExposureMode:
> - *         Normal:
> - *           exposureTime: [ 100, 10000, 30000, 60000, 120000 ]
> - *           gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ]
> - *         Short:
> - *           exposureTime: [ 100, 10000, 30000, 60000, 120000 ]
> - *           gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ]
> - *
> - * \endcode
> - *
> - * \return 0 on success or a negative error code
> - */
> -int AgcMeanLuminance::parseTuningData(const ValueNode &tuningData)
> -{
> -	int ret;
> -
> -	ret = parseRelativeLuminanceTarget(tuningData);
> -	if (ret)
> -		return ret;
> -
> -	ret = parseConstraintModes(tuningData);
> -	if (ret)
> -		return ret;
> -
> -	return parseExposureModes(tuningData);
> -}
> -
>  /**
>   * \brief Set the ExposureModeHelper limits for this class
>   * \param[in] minExposureTime Minimum exposure time to allow
> @@ -452,12 +214,12 @@ void AgcMeanLuminance::setLimits(utils::Duration minExposureTime,
>
>  /**
>   * \fn AgcMeanLuminance::constraintModes()
> - * \brief Get the constraint modes that have been parsed from tuning data
> + * \brief Get the available constraint modes
>   */
>
>  /**
>   * \fn AgcMeanLuminance::exposureModeHelpers()
> - * \brief Get the ExposureModeHelpers that have been parsed from tuning data
> + * \brief Get the available ExposureModeHelpers
>   */
>
>  /**
> diff --git a/src/ipa/libipa/agc_mean_luminance.h b/src/ipa/libipa/agc_mean_luminance.h
> index f799618ffc..c32bf44a2e 100644
> --- a/src/ipa/libipa/agc_mean_luminance.h
> +++ b/src/ipa/libipa/agc_mean_luminance.h
> @@ -8,13 +8,10 @@
>  #pragma once
>
>  #include <map>
> -#include <memory>
>  #include <vector>
>
>  #include <libcamera/base/utils.h>
>
> -#include "libcamera/internal/value_node.h"
> -
>  #include "exposure_mode_helper.h"
>  #include "histogram.h"
>  #include "pwl.h"
> @@ -26,9 +23,6 @@ namespace ipa {
>  class AgcMeanLuminance
>  {
>  public:
> -	AgcMeanLuminance();
> -	~AgcMeanLuminance();
> -
>  	struct AgcConstraint {
>  		enum class Bound {
>  			Lower = 0,
> @@ -40,6 +34,15 @@ public:
>  		Pwl yTarget;
>  	};
>
> +	struct Configuration {
> +		Pwl relativeLuminanceTarget;
> +		std::map<int32_t, std::vector<AgcConstraint>> constraintModes;
> +		std::map<int32_t, ExposureModeHelper> exposureModeHelpers;
> +	};
> +
> +	AgcMeanLuminance(Configuration &&config);
> +	~AgcMeanLuminance();
> +
>  	struct Traits {
>  		virtual ~Traits() = default;
>  		virtual double estimateLuminance(double gain) const = 0;
> @@ -80,20 +83,17 @@ public:
>  	double effectiveYTarget(double lux, double exposureCompensation) const;
>
>  private:
> -	int parseRelativeLuminanceTarget(const ValueNode &tuningData);
> -	int parseConstraint(const ValueNode &modeDict, int32_t id);
> -	int parseConstraintModes(const ValueNode &tuningData);
> -	int parseExposureModes(const ValueNode &tuningData);
>  	double estimateInitialGain(const Traits &traits, double yTarget) const;
>  	double constraintClampGain(const Params &params, double gain) const;
>  	utils::Duration filterExposure(utils::Duration exposureValue);
>
>  	utils::Duration filteredExposure_;
>  	mutable bool luxWarningEnabled_;
> -	Pwl relativeLuminanceTarget_;
>  	uint64_t frameCount_;
>
>  	std::vector<AgcConstraint> additionalConstraints_;
> +
> +	Pwl relativeLuminanceTarget_;

Not sure if this move is intentional, but it's just a detail

Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>

>  	std::map<int32_t, std::vector<AgcConstraint>> constraintModes_;
>  	std::map<int32_t, ExposureModeHelper> exposureModeHelpers_;
>  };
> --
> 2.56.0
>

Patch
diff mbox series

diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
index 7438825d30..54aa8207d4 100644
--- a/src/ipa/libipa/agc.cpp
+++ b/src/ipa/libipa/agc.cpp
@@ -26,6 +26,8 @@ 
 
 #include <libcamera/ipa/core_ipa_interface.h>
 
+#include "exposure_mode_helper.h"
+
 /**
  * \file agc.h
  * \brief libipa AGC algorithm
@@ -222,6 +224,196 @@  namespace agc {
 
 } /* namespace agc */
 
+namespace {
+
+/*
+ * Default relative luminance target
+ *
+ * This value should be chosen so that when the camera points at a grey target,
+ * the resulting image brightness looks "right". Custom values can be passed
+ * as the relativeLuminanceTarget value in sensor tuning files.
+ */
+static constexpr double kDefaultRelativeLuminanceTarget = 0.16;
+
+int parseRelativeLuminanceTarget(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData)
+{
+	auto &target = tuningData["relativeLuminanceTarget"];
+	if (!target) {
+		config.relativeLuminanceTarget = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } };
+		return 0;
+	}
+
+	auto pwl = target.get<Pwl>();
+	if (!pwl) {
+		LOG(Agc, Error)
+			<< "Failed to load relative luminance target.";
+		return -EINVAL;
+	}
+
+	config.relativeLuminanceTarget = std::move(*pwl);
+	return 0;
+}
+
+int parseConstraint(AgcMeanLuminance::Configuration &config, const ValueNode &modeDict, int32_t id)
+{
+	for (const auto &[boundName, content] : modeDict.asDict()) {
+		if (boundName != "upper" && boundName != "lower") {
+			LOG(Agc, Warning)
+				<< "Ignoring unknown constraint bound '" << boundName << "'";
+			continue;
+		}
+
+		unsigned int idx = static_cast<unsigned int>(boundName == "upper");
+		auto bound = static_cast<AgcMeanLuminance::AgcConstraint::Bound>(idx);
+		double qLo = content["qLo"].get<double>().value_or(0.98);
+		double qHi = content["qHi"].get<double>().value_or(1.0);
+		auto yTarget = content["yTarget"].get<Pwl>();
+		if (!yTarget) {
+			LOG(Agc, Error)
+				<< "Failed to parse yTarget";
+			return -EINVAL;
+		}
+
+		AgcMeanLuminance::AgcConstraint constraint = {
+			bound,
+			qLo,
+			qHi,
+			std::move(*yTarget),
+		};
+		auto &constraints = config.constraintModes[id];
+
+		if (idx)
+			constraints.push_back(std::move(constraint));
+		else
+			constraints.insert(constraints.begin(), std::move(constraint));
+	}
+
+	return 0;
+}
+
+int parseConstraintModes(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData)
+{
+	const ValueNode &constraintModes = tuningData[controls::AeConstraintMode.name()];
+	if (constraintModes.isDictionary()) {
+		for (const auto &[modeName, modeDict] : constraintModes.asDict()) {
+			auto it = controls::AeConstraintModeNameValueMap.find(modeName);
+			if (it == controls::AeConstraintModeNameValueMap.end()) {
+				LOG(Agc, Warning)
+					<< "Skipping unknown constraint mode '" << modeName << "'";
+				continue;
+			}
+
+			if (!modeDict.isDictionary()) {
+				LOG(Agc, Error)
+					<< "Invalid constraint mode '" << modeName << "'";
+				return -EINVAL;
+			}
+
+			int ret = parseConstraint(config, modeDict, it->second);
+			if (ret)
+				return ret;
+		}
+	}
+
+	/*
+	 * If the tuning data file contains no constraints then we use the
+	 * default constraint that the IPU3/RkISP1 Agc algorithms were adhering
+	 * to anyway before centralisation; this constraint forces the top 2% of
+	 * the histogram to be at least 0.5.
+	 */
+	if (config.constraintModes.empty()) {
+		AgcMeanLuminance::AgcConstraint constraint = {
+			AgcMeanLuminance::AgcConstraint::Bound::Lower,
+			0.98,
+			1.0,
+			Pwl({ { { 0.0, 0.5 } } }),
+		};
+
+		config.constraintModes[controls::AeConstraintModeNormal].push_back(
+			std::move(constraint));
+	}
+
+	return 0;
+}
+
+int parseExposureModes(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData)
+{
+	const ValueNode &exposureModes = tuningData[controls::AeExposureMode.name()];
+	if (exposureModes.isDictionary()) {
+		for (const auto &[modeName, modeValues] : exposureModes.asDict()) {
+			auto it = controls::AeExposureModeNameValueMap.find(modeName);
+			if (it == controls::AeExposureModeNameValueMap.end()) {
+				LOG(Agc, Warning)
+					<< "Skipping unknown exposure mode '" << modeName << "'";
+				continue;
+			}
+
+			if (!modeValues.isDictionary()) {
+				LOG(Agc, Error)
+					<< "Invalid exposure mode '" << modeName << "'";
+				return -EINVAL;
+			}
+
+			std::vector<uint32_t> exposureTimes =
+				modeValues["exposureTime"].get<std::vector<uint32_t>>().value_or(utils::defopt);
+			std::vector<double> gains =
+				modeValues["gain"].get<std::vector<double>>().value_or(utils::defopt);
+
+			if (exposureTimes.size() != gains.size()) {
+				LOG(Agc, Error)
+					<< "Exposure time and gain array sizes unequal";
+				return -EINVAL;
+			}
+
+			if (exposureTimes.empty()) {
+				LOG(Agc, Error)
+					<< "Exposure time and gain arrays are empty";
+				return -EINVAL;
+			}
+
+			std::vector<ExposureModeHelper::Stage> stages;
+			for (size_t i = 0; i < exposureTimes.size(); i++) {
+				stages.push_back({
+					.exposureTime = std::chrono::microseconds(exposureTimes[i]),
+					.gain = gains[i],
+				});
+			}
+
+			config.exposureModeHelpers.try_emplace(it->second, std::move(stages));
+		}
+	}
+
+	/*
+	 * If we don't have any exposure modes in the tuning data we create an
+	 * ExposureModeHelper using an empty vector of stages. This will result
+	 * in the ExposureModeHelper simply driving the exposure time as high as
+	 * possible before touching gain.
+	 */
+	if (config.exposureModeHelpers.empty())
+		config.exposureModeHelpers.try_emplace(
+			controls::AeExposureModeNormal,
+			std::vector<ExposureModeHelper::Stage>{});
+
+	return 0;
+}
+
+int parseTuningData(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData)
+{
+	int ret;
+
+	ret = parseRelativeLuminanceTarget(config, tuningData);
+	if (ret)
+		return ret;
+
+	ret = parseConstraintModes(config, tuningData);
+	if (ret)
+		return ret;
+
+	return parseExposureModes(config, tuningData);
+}
+
+} /* namespace */
+
 /**
  * \class AgcAlgorithm
  * \brief libIPA LSC algorithm algorithm
@@ -238,6 +430,57 @@  namespace agc {
  * virtual function, they should simply call the identically named member
  * function of AgcAlgorithm.
  *
+ * When a CameraSensorHelper is provided, AgcMeanLuminance is used and support for
+ * exposure (controls::AeExposureMode) and constraint (controls::AeConstraintMode)
+ * modes, and specifying the default luminance target is available via the tuning data.
+ *
+ * For controls::AeConstraintMode, the dictionary under the key "AeConstraintMode"
+ * is parsed: per-mode setting dictionaries with the key being a value from
+ * controls::AeConstraintModeNameValueMap. The yTarget can either be provided
+ * as single value or as array in which case it is interpreted as a PWL mapping
+ * lux levels to yTarget values. Each mode dict may contain either a "lower" or
+ * "upper" key or both, for example:
+ *
+ * \code{.unparsed}
+ * algorithms:
+ *   - Agc:
+ *       AeConstraintMode:
+ *         Normal:
+ *           lower:
+ *             qLo: 0.98
+ *             qHi: 1.0
+ *             yTarget: 0.5
+ *         Highlight:
+ *           lower:
+ *             qLo: 0.98
+ *             qHi: 1.0
+ *             yTarget: 0.5
+ *           upper:
+ *             qLo: 0.98
+ *             qHi: 1.0
+ *             yTarget: [ 100, 0.8, 20000, 0.5 ]
+ *
+ * \endcode
+ *
+ * For controls::AeExposureMode, the dictionary under the key AeExposureMode
+ * is parsed: per-mode setting dictionaries with the key being a value from
+ * controls::AeExposureModeNameValueMap. Each mode dict should contain
+ * an array of exposure times with the key "exposureTime" and an array of gain
+ * values with the key "gain", in this format:
+ *
+ * \code{.unparsed}
+ * algorithms:
+ *   - Agc:
+ *       AeExposureMode:
+ *         Normal:
+ *           exposureTime: [ 100, 10000, 30000, 60000, 120000 ]
+ *           gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ]
+ *         Short:
+ *           exposureTime: [ 100, 10000, 30000, 60000, 120000 ]
+ *           gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ]
+ *
+ * \endcode
+ *
  * \todo DigitalGain, DigitalGainMode
  * \todo Expand documentation
  */
@@ -298,9 +541,8 @@  namespace agc {
  * in ConfigurationParams::ctrlMap.
  *
  * If a CameraSensorHelper is provided in \a sensor, an instance of
- * AgcMeanLuminance is instantiated and the tuning data is loaded by
- * calling AgcMeanLuminance::parseTuningData(). Otherwise an instance
- * of AgcMSV is instantiated, and in this case the tuning data is ignored.
+ * AgcMeanLuminance is instantiated. Otherwise an instance of AgcMSV
+ * is instantiated, and in this case the tuning data is ignored.
  *
  * \return 0 on success, or a negative error code
  *
@@ -310,10 +552,12 @@  int AgcAlgorithm::init(const ValueNode &tuningData, CameraSensorHelper *sensor,
 		       const ConfigurationParams &config)
 {
 	if (sensor) {
-		auto &impl = impl_.emplace<AgcMeanLuminance>();
-		int ret = impl.parseTuningData(tuningData);
+		AgcMeanLuminance::Configuration agcConfig;
+		int ret = parseTuningData(agcConfig, tuningData);
 		if (ret)
 			return ret;
+
+		impl_.emplace<AgcMeanLuminance>(std::move(agcConfig));
 	} else {
 		impl_.emplace<AgcMSV>();
 	}
diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp
index 4c47770424..4f2b5edef6 100644
--- a/src/ipa/libipa/agc_mean_luminance.cpp
+++ b/src/ipa/libipa/agc_mean_luminance.cpp
@@ -9,7 +9,6 @@ 
 
 #include <algorithm>
 #include <cmath>
-#include <span>
 
 #include <libcamera/base/log.h>
 #include <libcamera/base/utils.h>
@@ -18,8 +17,6 @@ 
 
 #include "exposure_mode_helper.h"
 
-using namespace libcamera::controls;
-
 /**
  * \file agc_mean_luminance.h
  * \brief Class implementing mean luminance AEGC
@@ -39,15 +36,6 @@  namespace ipa {
  */
 static constexpr uint32_t kNumStartupFrames = 10;
 
-/*
- * Default relative luminance target
- *
- * This value should be chosen so that when the camera points at a grey target,
- * the resulting image brightness looks "right". Custom values can be passed
- * as the relativeLuminanceTarget value in sensor tuning files.
- */
-static constexpr double kDefaultRelativeLuminanceTarget = 0.16;
-
 /*
  * Maximum relative luminance target
  *
@@ -148,17 +136,13 @@  static constexpr unsigned int kDefaultLuxLevel = 500;
  * section of a histogram to a target value, where the target and the boundaries
  * of the section of the histogram used in the calculation are taken from the
  * values defined for the currently configured AeConstraintMode within the
- * tuning data. This class provides a helper function to parse those tuning data
- * to discover the constraints, and so requires a specific format for those
- * data which is described in \ref parseTuningData(). The gain from the first
- * stage is then clamped to the gain from this stage.
+ * tuning data. The gain from the first stage is then clamped to the gain from
+ * this stage.
  *
  * The final gain is used to adjust the effective exposure value of the image,
  * and that new exposure value is divided into exposure time, analogue gain and
  * digital gain according to the selected AeExposureMode. This class uses the
- * \ref ExposureModeHelper class to assist in that division, and expects the
- * data needed to initialise that class to be present in tuning data in a
- * format described in \ref parseTuningData().
+ * \ref ExposureModeHelper class to assist in that division.
  *
  * In order to be able to use this algorithm an IPA module needs to be able to
  * do the following:
@@ -170,175 +154,23 @@  static constexpr unsigned int kDefaultLuxLevel = 500;
  *
  * IPA modules that want to use this class to implement their AEGC algorithm
  * should derive AgcMeanLuminance::Traits and override the necessary functions.
- * The users must call parseTuningData() in init(), and must also call configure()
- * and setLimits() in configure(). They may then use calculateNewEv() in process().
- * If the limits passed to setLimits() change for any reason (for example, in
- * response to a FrameDurationLimit control being passed in queueRequest()) then
- * setLimits() must be called again with the new values.
+ * The users must call configure() and setLimits() in configure(). They may
+ * then use calculateNewEv() in process(). If the limits passed to setLimits()
+ * change for any reason (for example, in response to a FrameDurationLimit control
+ * being passed in queueRequest()) then setLimits() must be called again with the
+ * new values.
  */
 
-AgcMeanLuminance::AgcMeanLuminance()
-	: filteredExposure_(0s), luxWarningEnabled_(true), frameCount_(0)
+AgcMeanLuminance::AgcMeanLuminance(Configuration &&config)
+	: filteredExposure_(0s), luxWarningEnabled_(true), frameCount_(0),
+	  relativeLuminanceTarget_(std::move(config.relativeLuminanceTarget)),
+	  constraintModes_(std::move(config.constraintModes)),
+	  exposureModeHelpers_(std::move(config.exposureModeHelpers))
 {
 }
 
 AgcMeanLuminance::~AgcMeanLuminance() = default;
 
-int AgcMeanLuminance::parseRelativeLuminanceTarget(const ValueNode &tuningData)
-{
-	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;
-}
-
-int AgcMeanLuminance::parseConstraint(const ValueNode &modeDict, int32_t id)
-{
-	for (const auto &[boundName, content] : modeDict.asDict()) {
-		if (boundName != "upper" && boundName != "lower") {
-			LOG(AgcMeanLuminance, Warning)
-				<< "Ignoring unknown constraint bound '" << boundName << "'";
-			continue;
-		}
-
-		unsigned int idx = static_cast<unsigned int>(boundName == "upper");
-		AgcConstraint::Bound bound = static_cast<AgcConstraint::Bound>(idx);
-		double qLo = content["qLo"].get<double>().value_or(0.98);
-		double qHi = content["qHi"].get<double>().value_or(1.0);
-		auto yTarget = content["yTarget"].get<Pwl>();
-		if (!yTarget) {
-			LOG(AgcMeanLuminance, Error)
-				<< "Failed to parse yTarget";
-			return -EINVAL;
-		}
-
-		AgcConstraint constraint = { bound, qLo, qHi, std::move(*yTarget) };
-		auto &constraints = constraintModes_[id];
-
-		if (idx)
-			constraints.push_back(std::move(constraint));
-		else
-			constraints.insert(constraints.begin(), std::move(constraint));
-	}
-
-	return 0;
-}
-
-int AgcMeanLuminance::parseConstraintModes(const ValueNode &tuningData)
-{
-	const ValueNode &constraintModes = tuningData[controls::AeConstraintMode.name()];
-	if (constraintModes.isDictionary()) {
-		for (const auto &[modeName, modeDict] : constraintModes.asDict()) {
-			auto it = AeConstraintModeNameValueMap.find(modeName);
-			if (it == AeConstraintModeNameValueMap.end()) {
-				LOG(AgcMeanLuminance, Warning)
-					<< "Skipping unknown constraint mode '" << modeName << "'";
-				continue;
-			}
-
-			if (!modeDict.isDictionary()) {
-				LOG(AgcMeanLuminance, Error)
-					<< "Invalid constraint mode '" << modeName << "'";
-				return -EINVAL;
-			}
-
-			int ret = parseConstraint(modeDict, it->second);
-			if (ret)
-				return ret;
-		}
-	}
-
-	/*
-	 * If the tuning data file contains no constraints then we use the
-	 * default constraint that the IPU3/RkISP1 Agc algorithms were adhering
-	 * to anyway before centralisation; this constraint forces the top 2% of
-	 * the histogram to be at least 0.5.
-	 */
-	if (constraintModes_.empty()) {
-		AgcConstraint constraint = {
-			AgcConstraint::Bound::Lower,
-			0.98,
-			1.0,
-			Pwl({ { { 0.0, 0.5 } } })
-		};
-
-		constraintModes_[controls::AeConstraintModeNormal].push_back(std::move(constraint));
-	}
-
-	return 0;
-}
-
-int AgcMeanLuminance::parseExposureModes(const ValueNode &tuningData)
-{
-	const ValueNode &exposureModes = tuningData[controls::AeExposureMode.name()];
-	if (exposureModes.isDictionary()) {
-		for (const auto &[modeName, modeValues] : exposureModes.asDict()) {
-			auto it = AeExposureModeNameValueMap.find(modeName);
-			if (it == AeExposureModeNameValueMap.end()) {
-				LOG(AgcMeanLuminance, Warning)
-					<< "Skipping unknown exposure mode '" << modeName << "'";
-				continue;
-			}
-
-			if (!modeValues.isDictionary()) {
-				LOG(AgcMeanLuminance, Error)
-					<< "Invalid exposure mode '" << modeName << "'";
-				return -EINVAL;
-			}
-
-			std::vector<uint32_t> exposureTimes =
-				modeValues["exposureTime"].get<std::vector<uint32_t>>().value_or(utils::defopt);
-			std::vector<double> gains =
-				modeValues["gain"].get<std::vector<double>>().value_or(utils::defopt);
-
-			if (exposureTimes.size() != gains.size()) {
-				LOG(AgcMeanLuminance, Error)
-					<< "Exposure time and gain array sizes unequal";
-				return -EINVAL;
-			}
-
-			if (exposureTimes.empty()) {
-				LOG(AgcMeanLuminance, Error)
-					<< "Exposure time and gain arrays are empty";
-				return -EINVAL;
-			}
-
-			std::vector<ExposureModeHelper::Stage> stages;
-			for (size_t i = 0; i < exposureTimes.size(); i++) {
-				stages.push_back({
-					.exposureTime = std::chrono::microseconds(exposureTimes[i]),
-					.gain = gains[i],
-				});
-			}
-
-			exposureModeHelpers_.try_emplace(it->second, std::move(stages));
-		}
-	}
-
-	/*
-	 * If we don't have any exposure modes in the tuning data we create an
-	 * ExposureModeHelper using an empty vector of stages. This will result
-	 * in the ExposureModeHelper simply driving the exposure time as high as
-	 * possible before touching gain.
-	 */
-	if (exposureModeHelpers_.empty())
-		exposureModeHelpers_.try_emplace(controls::AeExposureModeNormal,
-						 std::vector<ExposureModeHelper::Stage>{});
-
-	return 0;
-}
-
 /**
  * \brief Configure the exposure mode helpers
  * \param[in] lineDuration The sensor line length
@@ -358,76 +190,6 @@  void AgcMeanLuminance::configure(utils::Duration lineDuration,
 	frameCount_ = 0;
 }
 
-/**
- * \brief Parse tuning data for AeConstraintMode and AeExposureMode controls
- * \param[in] tuningData the ValueNode representing the tuning data
- *
- * This function parses tuning data to build the list of allowed values for the
- * AeConstraintMode and AeExposureMode controls. Those tuning data must provide
- * the data in a specific format; the Agc algorithm's tuning data should contain
- * a dictionary called AeConstraintMode containing per-mode setting dictionaries
- * with the key being a value from \ref controls::AeConstraintModeNameValueMap.
- * The yTarget can either be provided as single value or as array in which case
- * it is interpreted as a PWL mapping lux levels to yTarget values. Each mode
- * dict may contain either a "lower" or "upper" key or both, for example:
- *
- * \code{.unparsed}
- * algorithms:
- *   - Agc:
- *       AeConstraintMode:
- *         Normal:
- *           lower:
- *             qLo: 0.98
- *             qHi: 1.0
- *             yTarget: 0.5
- *         Highlight:
- *           lower:
- *             qLo: 0.98
- *             qHi: 1.0
- *             yTarget: 0.5
- *           upper:
- *             qLo: 0.98
- *             qHi: 1.0
- *             yTarget: [ 100, 0.8, 20000, 0.5 ]
- *
- * \endcode
- *
- * For the AeExposureMode control the data should contain a dictionary called
- * AeExposureMode containing per-mode setting dictionaries with the key being a
- * value from \ref controls::AeExposureModeNameValueMap. Each mode dict should
- * contain an array of exposure times with the key "exposureTime" and an array
- * of gain values with the key "gain", in this format:
- *
- * \code{.unparsed}
- * algorithms:
- *   - Agc:
- *       AeExposureMode:
- *         Normal:
- *           exposureTime: [ 100, 10000, 30000, 60000, 120000 ]
- *           gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ]
- *         Short:
- *           exposureTime: [ 100, 10000, 30000, 60000, 120000 ]
- *           gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ]
- *
- * \endcode
- *
- * \return 0 on success or a negative error code
- */
-int AgcMeanLuminance::parseTuningData(const ValueNode &tuningData)
-{
-	int ret;
-
-	ret = parseRelativeLuminanceTarget(tuningData);
-	if (ret)
-		return ret;
-
-	ret = parseConstraintModes(tuningData);
-	if (ret)
-		return ret;
-
-	return parseExposureModes(tuningData);
-}
-
 /**
  * \brief Set the ExposureModeHelper limits for this class
  * \param[in] minExposureTime Minimum exposure time to allow
@@ -452,12 +214,12 @@  void AgcMeanLuminance::setLimits(utils::Duration minExposureTime,
 
 /**
  * \fn AgcMeanLuminance::constraintModes()
- * \brief Get the constraint modes that have been parsed from tuning data
+ * \brief Get the available constraint modes
  */
 
 /**
  * \fn AgcMeanLuminance::exposureModeHelpers()
- * \brief Get the ExposureModeHelpers that have been parsed from tuning data
+ * \brief Get the available ExposureModeHelpers
  */
 
 /**
diff --git a/src/ipa/libipa/agc_mean_luminance.h b/src/ipa/libipa/agc_mean_luminance.h
index f799618ffc..c32bf44a2e 100644
--- a/src/ipa/libipa/agc_mean_luminance.h
+++ b/src/ipa/libipa/agc_mean_luminance.h
@@ -8,13 +8,10 @@ 
 #pragma once
 
 #include <map>
-#include <memory>
 #include <vector>
 
 #include <libcamera/base/utils.h>
 
-#include "libcamera/internal/value_node.h"
-
 #include "exposure_mode_helper.h"
 #include "histogram.h"
 #include "pwl.h"
@@ -26,9 +23,6 @@  namespace ipa {
 class AgcMeanLuminance
 {
 public:
-	AgcMeanLuminance();
-	~AgcMeanLuminance();
-
 	struct AgcConstraint {
 		enum class Bound {
 			Lower = 0,
@@ -40,6 +34,15 @@  public:
 		Pwl yTarget;
 	};
 
+	struct Configuration {
+		Pwl relativeLuminanceTarget;
+		std::map<int32_t, std::vector<AgcConstraint>> constraintModes;
+		std::map<int32_t, ExposureModeHelper> exposureModeHelpers;
+	};
+
+	AgcMeanLuminance(Configuration &&config);
+	~AgcMeanLuminance();
+
 	struct Traits {
 		virtual ~Traits() = default;
 		virtual double estimateLuminance(double gain) const = 0;
@@ -80,20 +83,17 @@  public:
 	double effectiveYTarget(double lux, double exposureCompensation) const;
 
 private:
-	int parseRelativeLuminanceTarget(const ValueNode &tuningData);
-	int parseConstraint(const ValueNode &modeDict, int32_t id);
-	int parseConstraintModes(const ValueNode &tuningData);
-	int parseExposureModes(const ValueNode &tuningData);
 	double estimateInitialGain(const Traits &traits, double yTarget) const;
 	double constraintClampGain(const Params &params, double gain) const;
 	utils::Duration filterExposure(utils::Duration exposureValue);
 
 	utils::Duration filteredExposure_;
 	mutable bool luxWarningEnabled_;
-	Pwl relativeLuminanceTarget_;
 	uint64_t frameCount_;
 
 	std::vector<AgcConstraint> additionalConstraints_;
+
+	Pwl relativeLuminanceTarget_;
 	std::map<int32_t, std::vector<AgcConstraint>> constraintModes_;
 	std::map<int32_t, ExposureModeHelper> exposureModeHelpers_;
 };