From patchwork Wed Mar 4 23:01:29 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Javier Tia X-Patchwork-Id: 26257 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 C9D6ABE086 for ; Wed, 4 Mar 2026 23:25:29 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 8103C625C1; Thu, 5 Mar 2026 00:25:29 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=jetm.me header.i=@jetm.me header.b="LsR79zdG"; dkim=pass (2048-bit key; unprotected) header.d=messagingengine.com header.i=@messagingengine.com header.b="YHJLDMAm"; dkim-atps=neutral Received: from fhigh-b2-smtp.messagingengine.com (fhigh-b2-smtp.messagingengine.com [202.12.124.153]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 196CC623A8 for ; Thu, 5 Mar 2026 00:25:28 +0100 (CET) Received: from phl-compute-02.internal (phl-compute-02.internal [10.202.2.42]) by mailfhigh.stl.internal (Postfix) with ESMTP id 371FD7A008A for ; Wed, 4 Mar 2026 18:25:27 -0500 (EST) Received: from phl-imap-07 ([10.202.2.97]) by phl-compute-02.internal (MEProxy); Wed, 04 Mar 2026 18:25:27 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=jetm.me; h=cc :content-transfer-encoding:content-type:content-type:date:date :from:from:in-reply-to:in-reply-to:message-id:mime-version :references:reply-to:subject:subject:to:to; s=fm3; t=1772666727; x=1772753127; bh=OaeDzIOakd+ONgmodunrnm7nSLAmRXKVvpXL1/Le0Aw=; b= LsR79zdGYULr5aJM8o9xRcVUdOjNNj5eKz9G8FfigwdYqDgV3DNkrqNNIRkTASnb sY5nm3KCw6aoFcNbrmPtiJAoD/l01qopYCEFboaGU/K5/G5xbtRtKjXt/Z1Ye4pj qUrGzcjbein/Sf3AOxUV1WT0GZ5lMWx759elfL46TTvz0poCgS4somkvLF1gDjG1 caIdNNHbg6bi+j7JI54Ub7782kZ9SXasyhQ1Ll/Irs1IlHTX099lvhc1yvKcJz8Q xlkQX4WLYuRnNV8jCoNvt4xtFdoM7ey9KZGDjglhrRuatmBiNP21O0NLB4vVt7R/ 9u+WeaGf3f1XvycfO4eicw== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-transfer-encoding:content-type :content-type:date:date:feedback-id:feedback-id:from:from :in-reply-to:in-reply-to:message-id:mime-version:references :reply-to:subject:subject:to:to:x-me-proxy:x-me-sender :x-me-sender:x-sasl-enc; s=fm1; t=1772666727; x=1772753127; bh=O aeDzIOakd+ONgmodunrnm7nSLAmRXKVvpXL1/Le0Aw=; b=YHJLDMAmYJezYYXls 4qJkPYquRWK641Zh0UrLowpCYTUh09G/WnlJQfMnuRJKGfBZZgdUnkcoRDuGv7Jt xqg2U3bISYvHfHjwcfos82Rq+jFzK9Qqr79nqf8pzqdmOzdNu99JFJjDL+ipwkWo BYBOJbhhzq1AJ+60gl0ODKWSuwNLfzs/Amb5U4MDGjQPur0tn6NMNYHjZwG3ua5L zmMFAChoI4A5UzpOwl3e70VH2PFR0hs3qXJpb0kjvqdEEryEubWCXZIZQR4OoXeN d5mVI8r13JYMDe9omnPqHf6aibWTyNTKqD+ALXu9qFU0jycCl0luCkZv78M9wQIm gXU+A== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeefgedrtddtgddvieegkeduucetufdoteggodetrf dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu rghilhhouhhtmecufedttdenucgopfhokfffucdluddtmdenucfjughrpefotggggffhvf ffufgjfhesthektddtredtjeenucfhrhhomheplfgrvhhivghrucfvihgruceofhhlohhs shesjhgvthhmrdhmvgeqnecuggftrfgrthhtvghrnhepteetvdeklefgledtjefhhfdtle dvhfffgfeuieelgeeigfekgffhleeghfefhfejnecuvehluhhsthgvrhfuihiivgepuden ucfrrghrrghmpehmrghilhhfrhhomhepfhhlohhsshesjhgvthhmrdhmvgdpnhgspghrtg hpthhtohepuddpmhhouggvpehsmhhtphhouhhtpdhrtghpthhtoheplhhisggtrghmvghr rgdquggvvhgvlheslhhishhtshdrlhhisggtrghmvghrrgdrohhrgh X-ME-Proxy: Feedback-ID: i9dde48b3:Fastmail Received: by mailuser.phl.internal (Postfix, from userid 501) id E38A91EA006B; Wed, 4 Mar 2026 18:25:26 -0500 (EST) X-Mailer: MessagingEngine.com Webmail Interface MIME-Version: 1.0 From: Javier Tia To: libcamera-devel@lists.libcamera.org Date: Wed, 04 Mar 2026 17:01:29 -0600 Subject: [PATCH v2 1/4] ipa: simple: agc: Replace bang-bang controller with proportional In-Reply-To: 20260225221859.600869-1-floss@jetm.me References: 20260225221859.600869-1-floss@jetm.me Message-Id: <20260304232526.E38A91EA006B@mailuser.phl.internal> 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 AGC's updateExposure() uses a fixed ~10% step per frame regardless of how far the current exposure is from optimal. With a hysteresis dead band of only +/-4%, the controller overshoots when the correct value falls within one step, causing visible brightness oscillation (flicker). Replace the fixed-step bang-bang controller with a proportional one where the correction factor scales linearly with the MSV error: factor = 1.0 + clamp(error * 0.04, -0.15, +0.15) At maximum error (~2.5), this gives the same ~10% step as before. Near the target, steps shrink to <1%, eliminating overshoot. The existing hysteresis (kExposureSatisfactory) still prevents hunting on noise. Tested on OV2740 behind Intel IPU6 ISYS (ThinkPad X1 Carbon Gen 10) where the old controller produced continuous brightness flicker. The proportional controller converges in ~3 seconds from cold start with no visible oscillation. Signed-off-by: Javier Tia --- src/ipa/simple/algorithms/agc.cpp | 73 +++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/src/ipa/simple/algorithms/agc.cpp b/src/ipa/simple/algorithms/agc.cpp index 2f7e040c..a13a7552 100644 --- a/src/ipa/simple/algorithms/agc.cpp +++ b/src/ipa/simple/algorithms/agc.cpp @@ -7,6 +7,8 @@ #include "agc.h" +#include +#include #include #include @@ -37,52 +39,74 @@ static constexpr float kExposureOptimal = kExposureBinsCount / 2.0; */ static constexpr float kExposureSatisfactory = 0.2; +/* + * Proportional gain for exposure/gain adjustment. Maps the MSV error to a + * multiplicative correction factor: + * + * factor = 1.0 + kExpProportionalGain * error + * + * With kExpProportionalGain = 0.04: + * - max error ~2.5 -> factor 1.10 (~10% step, same as before) + * - error 1.0 -> factor 1.04 (~4% step) + * - error 0.3 -> factor 1.012 (~1.2% step) + * + * This replaces the fixed 10% bang-bang step with a proportional correction + * that converges smoothly and avoids overshooting near the target. + */ +static constexpr float kExpProportionalGain = 0.04; + +/* + * Maximum multiplicative step per frame, to bound the correction when the + * scene changes dramatically. + */ +static constexpr float kExpMaxStep = 0.15; + Agc::Agc() { } void Agc::updateExposure(IPAContext &context, IPAFrameContext &frameContext, double exposureMSV) { - /* - * kExpDenominator of 10 gives ~10% increment/decrement; - * kExpDenominator of 5 - about ~20% - */ - static constexpr uint8_t kExpDenominator = 10; - static constexpr uint8_t kExpNumeratorUp = kExpDenominator + 1; - static constexpr uint8_t kExpNumeratorDown = kExpDenominator - 1; - int32_t &exposure = frameContext.sensor.exposure; double &again = frameContext.sensor.gain; - if (exposureMSV < kExposureOptimal - kExposureSatisfactory) { + double error = kExposureOptimal - exposureMSV; + + if (std::abs(error) <= kExposureSatisfactory) + return; + + /* + * Compute a proportional correction factor. The sign of the error + * determines the direction: positive error means too dark (increase), + * negative means too bright (decrease). + */ + float step = std::clamp(static_cast(error) * kExpProportionalGain, + -kExpMaxStep, kExpMaxStep); + float factor = 1.0f + step; + + if (factor > 1.0f) { + /* Scene too dark: increase exposure first, then gain. */ if (exposure < context.configuration.agc.exposureMax) { - int32_t next = exposure * kExpNumeratorUp / kExpDenominator; - if (next - exposure < 1) - exposure += 1; - else - exposure = next; + int32_t next = static_cast(exposure * factor); + exposure = std::max(next, exposure + 1); } else { - double next = again * kExpNumeratorUp / kExpDenominator; + double next = again * factor; if (next - again < context.configuration.agc.againMinStep) again += context.configuration.agc.againMinStep; else again = next; } - } - - if (exposureMSV > kExposureOptimal + kExposureSatisfactory) { + } else { + /* Scene too bright: decrease gain first, then exposure. */ if (again > context.configuration.agc.again10) { - double next = again * kExpNumeratorDown / kExpDenominator; + double next = again * factor; if (again - next < context.configuration.agc.againMinStep) again -= context.configuration.agc.againMinStep; else again = next; } else { - int32_t next = exposure * kExpNumeratorDown / kExpDenominator; - if (exposure - next < 1) - exposure -= 1; - else - exposure = next; + int32_t next = static_cast(exposure * factor); + exposure = std::min(next, exposure - 1); } } @@ -96,6 +120,7 @@ void Agc::updateExposure(IPAContext &context, IPAFrameContext &frameContext, dou LOG(IPASoftExposure, Debug) << "exposureMSV " << exposureMSV + << " error " << error << " factor " << factor << " exp " << exposure << " again " << again; }