From patchwork Mon Oct 26 13:20:29 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28624 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 8BA78C324C for ; Mon, 26 Oct 2026 13:21:09 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 90D556901C; Mon, 26 Oct 2026 14:21:01 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="d6HS4xH4"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 3E0C568FF1 for ; Mon, 26 Oct 2026 14:20:50 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id DD7EF268 for ; Mon, 26 Oct 2026 14:18:38 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1793020719; bh=JhlAxOma+lTURh19hWjWgKqvn/mdpGxZeEjQJ9uohwM=; h=From:To:Subject:Date:In-Reply-To:References:From; b=d6HS4xH4sUOJhujlv49S9QkIIHwrej4I15kH6xOG8EWqIZ9m9/vKJtI7hG1fVSF+v P0z9/Ba69hT3ufz4C/h0bAGA77nosvpvJFyQWRUnJhXepd3nvHn6EN0ohMTukyIVLx XZ8m2UvXPTKJ0XEeCzlEeR28KDVkJkmYxujUz7bk= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Subject: [RFC PATCH v4 07/23] ipa: libipa: agc: Move tuning file parsing Date: Mon, 26 Oct 2026 14:20:29 +0100 Message-ID: <20261026132045.269007-8-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261026132045.269007-1-barnabas.pocze@ideasonboard.com> References: <20261026132045.269007-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Move the tuning file parsing out of `AgcMeanLuminance`, and provide the configuration via the constructor. Signed-off-by: Barnabás Pőcze --- src/ipa/libipa/agc.cpp | 257 +++++++++++++++++++++++++- src/ipa/libipa/agc_mean_luminance.cpp | 246 +----------------------- src/ipa/libipa/agc_mean_luminance.h | 22 +-- 3 files changed, 273 insertions(+), 252 deletions(-) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index e23cb13cb7..a27813b8bf 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -26,6 +26,8 @@ #include +#include "exposure_mode_helper.h" + /** * \file agc.h * \brief libipa AGC algorithm @@ -222,6 +224,253 @@ 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. + */ +constexpr double kDefaultRelativeLuminanceTarget = 0.16; + +int parseRelativeLuminanceTarget(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData) +{ + auto &target = tuningData; + if (!target) { + config.relativeLuminanceTarget = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } }; + return 0; + } + + auto pwl = target.get(); + 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(boundName == "upper"); + auto bound = static_cast(idx); + double qLo = content["qLo"].get().value_or(0.98); + double qHi = content["qHi"].get().value_or(1.0); + auto yTarget = content["yTarget"].get(); + 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 &constraintModes) +{ + if (!constraintModes.isDictionary()) + return 0; + + 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; + } + + return 0; +} + +int parseExposureModes(AgcMeanLuminance::Configuration &config, const ValueNode &exposureModes) +{ + if (!exposureModes.isDictionary()) + return 0; + + 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 exposureTimes = + modeValues["exposureTime"].get>().value_or(utils::defopt); + std::vector gains = + modeValues["gain"].get>().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 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)); + } + + return 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: + * ConstraintNormal: + * lower: + * qLo: 0.98 + * qHi: 1.0 + * yTarget: 0.5 + * ConstraintHighlight: + * 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: + * ExposureNormal: + * exposureTime: [ 100, 10000, 30000, 60000, 120000 ] + * gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ] + * ExposureShort: + * 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 parseTuningData(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData) +{ + int ret; + + ret = parseRelativeLuminanceTarget(config, tuningData["relativeLuminanceTarget"]); + if (ret) + return ret; + + ret = parseConstraintModes(config, tuningData[controls::AeConstraintMode.name()]); + if (ret) + return ret; + + return parseExposureModes(config, tuningData[controls::AeExposureMode.name()]); +} + +void fillMissing(AgcMeanLuminance::Configuration &config) +{ + /* + * 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::ExposureNormal, + std::vector{}); + + /* + * 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::ConstraintNormal].push_back(std::move(constraint)); + } +} + +} /* namespace */ + /** * \class AgcAlgorithm * \brief libIPA LSC algorithm algorithm @@ -310,10 +559,14 @@ int AgcAlgorithm::init(const ValueNode &tuningData, CameraSensorHelper *sensor, const ConfigurationParams &config) { if (sensor) { - auto &impl = impl_.emplace(); - int ret = impl.parseTuningData(tuningData); + AgcMeanLuminance::Configuration agcMLconfig; + int ret = parseTuningData(agcMLconfig, tuningData); if (ret) return ret; + + fillMissing(agcMLconfig); + + impl_.emplace(std::move(agcMLconfig)); } else { impl_.emplace(); } diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp index eff639fb67..8b35eab6aa 100644 --- a/src/ipa/libipa/agc_mean_luminance.cpp +++ b/src/ipa/libipa/agc_mean_luminance.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -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 * @@ -177,168 +165,18 @@ static constexpr unsigned int kDefaultLuxLevel = 500; * 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)) { + ASSERT(!constraintModes_.empty()); + ASSERT(!exposureModeHelpers_.empty()); } 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(); - 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(boundName == "upper"); - AgcConstraint::Bound bound = static_cast(idx); - double qLo = content["qLo"].get().value_or(0.98); - double qHi = content["qHi"].get().value_or(1.0); - auto yTarget = content["yTarget"].get(); - 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::ConstraintNormal].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 exposureTimes = - modeValues["exposureTime"].get>().value_or(utils::defopt); - std::vector gains = - modeValues["gain"].get>().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 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::ExposureNormal, - std::vector{}); - - return 0; -} - /** * \brief Configure the exposure mode helpers * \param[in] lineDuration The sensor line length @@ -358,76 +196,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: - * ConstraintNormal: - * lower: - * qLo: 0.98 - * qHi: 1.0 - * yTarget: 0.5 - * ConstraintHighlight: - * 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: - * ExposureNormal: - * exposureTime: [ 100, 10000, 30000, 60000, 120000 ] - * gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ] - * ExposureShort: - * 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 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 -#include #include #include -#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> constraintModes; + std::map 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 ¶ms, double gain) const; utils::Duration filterExposure(utils::Duration exposureValue); utils::Duration filteredExposure_; mutable bool luxWarningEnabled_; - Pwl relativeLuminanceTarget_; uint64_t frameCount_; std::vector additionalConstraints_; + + Pwl relativeLuminanceTarget_; std::map> constraintModes_; std::map exposureModeHelpers_; };