From patchwork Thu Nov 6 16:42:25 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Stefan Klug X-Patchwork-Id: 24981 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 A284CBDE4C for ; Thu, 6 Nov 2025 16:42:48 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 5472A60A85; Thu, 6 Nov 2025 17:42:48 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="Az8CKsTf"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id DBF23609D8 for ; Thu, 6 Nov 2025 17:42:46 +0100 (CET) Received: from ideasonboard.com (unknown [IPv6:2a00:6020:448c:6c00:1d00:3bae:f27:7601]) by perceval.ideasonboard.com (Postfix) with UTF8SMTPSA id C402E6AF; Thu, 6 Nov 2025 17:40:51 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1762447251; bh=HgIyUHgF5AeBeG4nogNb3WSdDsAU/wcMRq0HHU9DWyg=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=Az8CKsTfPMnB7we49d+zomXVEZGzesCYBx5ePGigxJudrJWSLETV3mcnKWBzDxyjH /iju1RoyYVuQMkDdDMq0OI8971jGQcCEGaCm10XJHyZGj45GVnUbzrpdkd7ikhXC7W 5Nu2Jq9WqEjO4YpGqD0m3WY0x/rqfdvu+GUZerXE= From: Stefan Klug To: libcamera-devel@lists.libcamera.org Cc: Stefan Klug Subject: [PATCH v2 1/3] ipa: libipa: pwl: Allow to parse a plain yaml value as single point PWL Date: Thu, 6 Nov 2025 17:42:25 +0100 Message-ID: <20251106164239.460738-2-stefan.klug@ideasonboard.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20251106164239.460738-1-stefan.klug@ideasonboard.com> References: <20251106164239.460738-1-stefan.klug@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" In the tuning files, it is useful to specify some values as PWL that produces a different output value depending on an input value. For example it is useful to specify the relativeLuminanceTarget depending on the lux level, so that the regulation regulates a bit darker in low light scenes. For simple setups this is not necessary and a single value is sufficient. This patch extends the yaml loading code, so that either case is supported transparently without the need for additional external code. This way the following yaml expressions are all valid: yTarget: [ 1000, 0.15, 2000, 0.17 ] # Regular PWL yTarget: [ 0, 0.17 ] # Single point PWL yTarget: 0.17 # Same as above For cases (I'm not aware of any) where a single point Pwl is not allowed there is no change as that must be checked externally anyways. Signed-off-by: Stefan Klug Reviewed-by: Kieran Bingham --- Changes in v2: - Added this patch --- src/ipa/libipa/pwl.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ipa/libipa/pwl.cpp b/src/ipa/libipa/pwl.cpp index 69a9334112e8..78ba43e39b77 100644 --- a/src/ipa/libipa/pwl.cpp +++ b/src/ipa/libipa/pwl.cpp @@ -437,6 +437,15 @@ template<> std::optional YamlObject::Getter::get(const YamlObject &obj) const { + /* Treat a single value as single point PWL */ + if (obj.isValue()) { + auto v = obj.get(); + if (!v) + return std::nullopt; + + return ipa::Pwl({ { { 0.0, *v } } }); + } + if (!obj.size() || obj.size() % 2) return std::nullopt; From patchwork Thu Nov 6 16:42:26 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Stefan Klug X-Patchwork-Id: 24982 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 36E8ABDE4C for ; Thu, 6 Nov 2025 16:42:51 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id E308E60A8B; Thu, 6 Nov 2025 17:42:50 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="R0zgdBP0"; 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 BA19D609D8 for ; Thu, 6 Nov 2025 17:42:49 +0100 (CET) Received: from ideasonboard.com (unknown [IPv6:2a00:6020:448c:6c00:1d00:3bae:f27:7601]) by perceval.ideasonboard.com (Postfix) with UTF8SMTPSA id 9A0097CE; Thu, 6 Nov 2025 17:40:54 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1762447254; bh=M6KX1O2mGKCVZ/D1uI2P+saU7mWUPYtGNeFPiJTo+ME=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=R0zgdBP0Fx2kF6PSbRsSKb+cV6qrmnyL5vK5VkLXNuI4/3ptVIFKG70VVJnVSGzMb Z+BoR4okER0u1sA5wkGPWeiWoaJxIMryBuK7Is6qDs7jp5ssdgRADxB1Hlh30Ta9mn h4baPLopfddtJzxss+XtT5r0WIHV1zz9f++J73TU= From: Stefan Klug To: libcamera-devel@lists.libcamera.org Cc: Stefan Klug , Paul Elder , Daniel Scally Subject: [PATCH v2 2/3] ipa: libipa: agc_mean_luminance: Change luminance target to piecewise linear function Date: Thu, 6 Nov 2025 17:42:26 +0100 Message-ID: <20251106164239.460738-3-stefan.klug@ideasonboard.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20251106164239.460738-1-stefan.klug@ideasonboard.com> References: <20251106164239.460738-1-stefan.klug@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" In some situations it is necessary to specify the target brightness value depending on the overall lux level. This is a rework [1] to fit current master. As the PWL loading code loads a plain value as single point PWL, backwards compatibility to existing tuning files is ensured. [1] https://patchwork.libcamera.org/patch/20231/ Signed-off-by: Paul Elder Signed-off-by: Stefan Klug Reviewed-by: Daniel Scally --- 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 | 51 +++++++++++++++++++++++---- src/ipa/libipa/agc_mean_luminance.h | 11 ++++-- src/ipa/rkisp1/algorithms/agc.cpp | 1 + 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp index 64f36bd75dd2..b80af92a2c0f 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 @@ -145,16 +153,24 @@ static constexpr double kMaxRelativeLuminanceTarget = 0.95; AgcMeanLuminance::AgcMeanLuminance() : exposureCompensation_(1.0), frameCount_(0), filteredExposure_(0s), - relativeLuminanceTarget_(0) + lux_(0) { } AgcMeanLuminance::~AgcMeanLuminance() = default; -void AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData) +int AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData) { - relativeLuminanceTarget_ = - tuningData["relativeLuminanceTarget"].get(kDefaultRelativeLuminanceTarget); + auto &target = tuningData["relativeLuminanceTarget"]; + auto pwl = target.get(); + 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) @@ -385,7 +401,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 +421,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 +566,18 @@ double AgcMeanLuminance::constraintClampGain(uint32_t constraintModeIndex, */ double AgcMeanLuminance::effectiveYTarget() const { - return std::min(relativeLuminanceTarget_ * exposureCompensation_, + double lux = lux_; + if (relativeLuminanceTarget_.size() > 1 && lux_ == 0) { + LOG(AgcMeanLuminance, Debug) + << "Missing lux value for luminance target calculation, default to " + << kDefaultLuxLevel; + 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 d7ec548e3e58..1fbff304a72b 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 constraints); @@ -81,8 +87,8 @@ public: private: virtual double estimateLuminance(const double gain) const = 0; + int parseRelativeLuminanceTarget(const YamlObject &tuningData); - void parseRelativeLuminanceTarget(const YamlObject &tuningData); void parseConstraint(const YamlObject &modeDict, int32_t id); int parseConstraintModes(const YamlObject &tuningData); int parseExposureModes(const YamlObject &tuningData); @@ -95,7 +101,8 @@ private: double exposureCompensation_; uint64_t frameCount_; utils::Duration filteredExposure_; - double relativeLuminanceTarget_; + unsigned int lux_; + Pwl relativeLuminanceTarget_; std::vector additionalConstraints_; std::map> 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; From patchwork Thu Nov 6 16:42:27 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Stefan Klug X-Patchwork-Id: 24983 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 06D9BBDE4C for ; Thu, 6 Nov 2025 16:42:54 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id A344D609D8; Thu, 6 Nov 2025 17:42:53 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="rvzMwGgF"; 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 AA570609D8 for ; Thu, 6 Nov 2025 17:42:52 +0100 (CET) Received: from ideasonboard.com (unknown [IPv6:2a00:6020:448c:6c00:1d00:3bae:f27:7601]) by perceval.ideasonboard.com (Postfix) with UTF8SMTPSA id 88B09E52; Thu, 6 Nov 2025 17:40:57 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1762447257; bh=/oyy8Wr3YBGRVtFKKyEqlknjgcInldFRGcmyfUatc1s=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=rvzMwGgFmjYnw91MBjJZjUw7Dc1lEDAwVJ9CwYtEJhBzFqkmuvhmZzUk1nE70iXVH MS2zgvhWxrahp/wclKuxZfLifq2U52JH0Qb1HH0IxLqmuRUkBSRvpBCngWM96eaVBT IriG8g2nSX2wpOvhK0pU2fA2R0Eg1gQxTy/+fTQw= From: Stefan Klug To: libcamera-devel@lists.libcamera.org Cc: Stefan Klug , Daniel Scally Subject: [PATCH v2 3/3] ipa: libipa: agc_mean_luminance: Fix yTarget handling in constraints Date: Thu, 6 Nov 2025 17:42:27 +0100 Message-ID: <20251106164239.460738-4-stefan.klug@ideasonboard.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20251106164239.460738-1-stefan.klug@ideasonboard.com> References: <20251106164239.460738-1-stefan.klug@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" The yTarget loading code is broken and works neither for plain values nor for arrays of values to form a PWL. Fix this by supporting both cases. If a list is provided in the tuning file construct a PWL, otherwise construct a single point PWL with the given value. Fixes: 24247a12c7d3 ("ipa: libipa: Add AgcMeanLuminance base class") Signed-off-by: Stefan Klug Reviewed-by: Daniel Scally Reviewed-by: Kieran Bingham --- Changes in v2: - Collected tag - Drop special logic to handle single values as that was added generically to the yaml Pwl loader --- src/ipa/libipa/agc_mean_luminance.cpp | 41 +++++++++++++++++++-------- src/ipa/libipa/agc_mean_luminance.h | 5 ++-- src/ipa/rkisp1/algorithms/wdr.cpp | 3 +- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp index b80af92a2c0f..7fac8c760396 100644 --- a/src/ipa/libipa/agc_mean_luminance.cpp +++ b/src/ipa/libipa/agc_mean_luminance.cpp @@ -173,7 +173,7 @@ int AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData) return 0; } -void AgcMeanLuminance::parseConstraint(const YamlObject &modeDict, int32_t id) +int AgcMeanLuminance::parseConstraint(const YamlObject &modeDict, int32_t id) { for (const auto &[boundName, content] : modeDict.asDict()) { if (boundName != "upper" && boundName != "lower") { @@ -186,10 +186,14 @@ void AgcMeanLuminance::parseConstraint(const YamlObject &modeDict, int32_t id) AgcConstraint::Bound bound = static_cast(idx); double qLo = content["qLo"].get().value_or(0.98); double qHi = content["qHi"].get().value_or(1.0); - double yTarget = - content["yTarget"].getList().value_or(std::vector{ 0.5 }).at(0); + auto yTarget = content["yTarget"].get(); + if (!yTarget) { + LOG(AgcMeanLuminance, Error) + << "Failed to parse yTarget"; + return -EINVAL; + } - AgcConstraint constraint = { bound, qLo, qHi, yTarget }; + AgcConstraint constraint = { bound, qLo, qHi, std::move(*yTarget) }; if (!constraintModes_.count(id)) constraintModes_[id] = {}; @@ -199,6 +203,8 @@ void AgcMeanLuminance::parseConstraint(const YamlObject &modeDict, int32_t id) else constraintModes_[id].insert(constraintModes_[id].begin(), constraint); } + + return 0; } int AgcMeanLuminance::parseConstraintModes(const YamlObject &tuningData) @@ -221,8 +227,11 @@ int AgcMeanLuminance::parseConstraintModes(const YamlObject &tuningData) return -EINVAL; } - parseConstraint(modeDict, - AeConstraintModeNameValueMap.at(modeName)); + int ret = parseConstraint(modeDict, + AeConstraintModeNameValueMap.at(modeName)); + if (ret) + return ret; + availableConstraintModes.push_back( AeConstraintModeNameValueMap.at(modeName)); } @@ -239,7 +248,7 @@ int AgcMeanLuminance::parseConstraintModes(const YamlObject &tuningData) AgcConstraint::Bound::Lower, 0.98, 1.0, - 0.5 + Pwl({ { { 0.0, 0.5 } } }) }; constraintModes_[controls::ConstraintNormal].insert( @@ -352,8 +361,9 @@ void AgcMeanLuminance::configure(utils::Duration lineDuration, * 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. - * Each mode dict may contain either a "lower" or "upper" key or both, for - * example: + * 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: @@ -372,7 +382,7 @@ void AgcMeanLuminance::configure(utils::Duration lineDuration, * upper: * qLo: 0.98 * qHi: 1.0 - * yTarget: 0.8 + * yTarget: [ 100, 0.8, 20000, 0.5 ] * * \endcode * @@ -528,8 +538,15 @@ double AgcMeanLuminance::constraintClampGain(uint32_t constraintModeIndex, const Histogram &hist, double gain) { - auto applyConstraint = [&gain, &hist](const AgcConstraint &constraint) { - double newGain = constraint.yTarget * hist.bins() / + auto applyConstraint = [this, &gain, &hist](const AgcConstraint &constraint) { + double lux = lux_; + + if (relativeLuminanceTarget_.size() > 1 && lux_ == 0) + lux = kDefaultLuxLevel; + + double target = constraint.yTarget.eval( + constraint.yTarget.domain().clamp(lux)); + double newGain = target * hist.bins() / hist.interQuantileMean(constraint.qLo, constraint.qHi); if (constraint.bound == AgcConstraint::Bound::Lower && diff --git a/src/ipa/libipa/agc_mean_luminance.h b/src/ipa/libipa/agc_mean_luminance.h index 1fbff304a72b..d0d1fc139509 100644 --- a/src/ipa/libipa/agc_mean_luminance.h +++ b/src/ipa/libipa/agc_mean_luminance.h @@ -40,7 +40,7 @@ public: Bound bound; double qLo; double qHi; - double yTarget; + Pwl yTarget; }; void configure(utils::Duration lineDuration, const CameraSensorHelper *sensorHelper); @@ -88,8 +88,7 @@ public: private: virtual double estimateLuminance(const double gain) const = 0; int parseRelativeLuminanceTarget(const YamlObject &tuningData); - - void parseConstraint(const YamlObject &modeDict, int32_t id); + int parseConstraint(const YamlObject &modeDict, int32_t id); int parseConstraintModes(const YamlObject &tuningData); int parseExposureModes(const YamlObject &tuningData); double estimateInitialGain() const; diff --git a/src/ipa/rkisp1/algorithms/wdr.cpp b/src/ipa/rkisp1/algorithms/wdr.cpp index 45144913dcd8..ed81628c032c 100644 --- a/src/ipa/rkisp1/algorithms/wdr.cpp +++ b/src/ipa/rkisp1/algorithms/wdr.cpp @@ -175,7 +175,8 @@ int WideDynamicRange::configure(IPAContext &context, constraint.bound = AgcMeanLuminance::AgcConstraint::Bound::Upper; constraint.qHi = 1.0; constraint.qLo = 1.0 - exposureConstraintMaxBrightPixels_; - constraint.yTarget = exposureConstraintY_; + constraint.yTarget.clear(); + constraint.yTarget.append(0, exposureConstraintY_); return 0; }