From patchwork Fri Mar 6 18:46:40 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Javier Tia X-Patchwork-Id: 26267 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 121D2BE086 for ; Fri, 6 Mar 2026 18:47:24 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id BE40C62635; Fri, 6 Mar 2026 19:47:23 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=jetm.me header.i=@jetm.me header.b="qHKdYu8N"; dkim=pass (2048-bit key; unprotected) header.d=messagingengine.com header.i=@messagingengine.com header.b="okr94qrS"; dkim-atps=neutral Received: from fhigh-b6-smtp.messagingengine.com (fhigh-b6-smtp.messagingengine.com [202.12.124.157]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id BFB4562620 for ; Fri, 6 Mar 2026 19:47:21 +0100 (CET) Received: from phl-compute-02.internal (phl-compute-02.internal [10.202.2.42]) by mailfhigh.stl.internal (Postfix) with ESMTP id CD8F57A0155; Fri, 6 Mar 2026 13:47:20 -0500 (EST) Received: from phl-imap-07 ([10.202.2.97]) by phl-compute-02.internal (MEProxy); Fri, 06 Mar 2026 13:47:20 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=jetm.me; h=cc: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=1772822840; x=1772909240; bh=h+BuvakQdxEZJo6r5m3X1LJOae1UMzkzzyG/TIjsqxo=; b= qHKdYu8N/vH4pnnS9qnO5IS+fO18gtvFPHHEVcgTq+hAB9YxealTQomaA0ihZzVH gHi604oark/cxYSwkvRJ3I19wYTnfPXVwmq14LYdGdgxqZufYGMRVL1tRbz6XqtK thEOE3Lf779mERRuRdIKxSI97UTPJKsCv2BCapKU5veCygsR5uLiY11U50NPcZlA USPGjOp0VmtVQ2etQ434iW3t4OlGKSWMzI7HBTP5/UvGY5SZH/Q8Z7GHDDHCvtzD uetXFD+kI8Fj65OxFtSj7UKOTVRFs3lvjxGRgYEOK5zgwro4DJFKuWjkLKFzyh1E 1I6Yk3tu6j2L+gCOcZQMIg== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc: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=1772822840; x= 1772909240; bh=h+BuvakQdxEZJo6r5m3X1LJOae1UMzkzzyG/TIjsqxo=; b=o kr94qrSn/ukFusm0xJjMLu1ycxVqC24Rh2LAQDQNOOpEJvdfbZP/vBrEl0SxErXb Y9Ff4vg0v96uVBjHmX7K44GyZEu18HruazOHRTgaV2NLxrh4d2zxD7ZbhA43L8d/ PzcOq5z7JQorao+BVAxU4jI7MTIV33lG3jF15YH/O4Zq1KlrhI5BtEAAHT9d560i iOdXDvWbbwEmN+CprAWiv8CRInmD8LN106z1idEBrb4GN7eVG2YzlZW5GZ5zRJqB v+/5mYm/GWVijvSPWLhlRdCK945gka5wWDxrevk+F0gU8IZWL7/1mutFMeNuhNW0 rq+faJO2POhwK34u0guXw== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeefgedrtddtgddvjedttdegucetufdoteggodetrf dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu rghilhhouhhtmecufedttdenucesvcftvggtihhpihgvnhhtshculddquddttddmnecujf gurhepoffhfffugggtgffkvfevofgjfhesthekredtredtjeenucfhrhhomheplfgrvhhi vghrucfvihgruceofhhlohhsshesjhgvthhmrdhmvgeqnecuggftrfgrthhtvghrnhepve ektdfhffevuddtgfetieevtddvheduhfeggfekveejlefhleefieevueefiefhnecuvehl uhhsthgvrhfuihiivgeptdenucfrrghrrghmpehmrghilhhfrhhomhepfhhlohhsshesjh gvthhmrdhmvgdpnhgspghrtghpthhtohepgedpmhhouggvpehsmhhtphhouhhtpdhrtghp thhtohepsggrrhhnrggsrghsrdhpohgtiigvsehiuggvrghsohhnsghorghrugdrtghomh dprhgtphhtthhopehflhhoshhssehjvghtmhdrmhgvpdhrtghpthhtoheplhhisggtrghm vghrrgdquggvvhgvlheslhhishhtshdrlhhisggtrghmvghrrgdrohhrghdprhgtphhtth hopehmiigrmhgriigrlhesrhgvughhrghtrdgtohhm X-ME-Proxy: Feedback-ID: i9dde48b3:Fastmail Received: by mailuser.phl.internal (Postfix, from userid 501) id 8B0911EA006B; Fri, 6 Mar 2026 13:47:20 -0500 (EST) X-Mailer: MessagingEngine.com Webmail Interface From: Javier Tia Date: Fri, 06 Mar 2026 12:46:40 -0600 Subject: [PATCH v4 1/3] ipa: simple: agc: Replace bang-bang controller with proportional MIME-Version: 1.0 Message-Id: <20260306-agc-proportional-v4-1-e87c7e0d837a@jetm.me> To: libcamera-devel@lists.libcamera.org Cc: Javier Tia , Milan Zamazal , =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Mailer: b4 0.14.3 X-Developer-Signature: v=1; a=openpgp-sha256; l=4996; i=floss@jetm.me; h=from:subject:message-id; bh=SV7Gcbd6KVxn2JTosswI0D9gL6WG/lCrexuHvmEVNEY=; b=owEB7QES/pANAwAKAbXuwwuoZ3cfAcsmYgBpqyEPA8AURRa+CBXFQq47ccTyPZN4PlUZOKtSJ kYBB4wIO+CJAbMEAAEKAB0WIQSbE7ILzw7eI0VKk8m17sMLqGd3HwUCaashDwAKCRC17sMLqGd3 H+/0DAC+haXU0rvymThx+t7rGyihSlVZW7DFrgdq2XRqhbbfbIn/QG/af6eMUKfKGD0GPLpsvsP +nxosfHz2rCoDfmHSeJSE4ZypVob+zZpl9Nw1Iqoqp78AaDyAaKlSuddAILz2luCaOD4mxGvk6W mJbDBHidoPY7z650JAcUcBiKYa/V0DzegOFc9BQ6yQhMpNhE8jnv69dEHEZ/PxIsMlbsLPDJjhQ RSiU9IuzMK9wPx1HdgoFFNML5sY0kHGt2LJ5Uj43jBVWOHYmlVfRy0wp/yQbEsv19853Me/3sO/ ddFL+NANLC6zuCEqP5w90PYBx1fBhN5Xe6cjD/SlO9LPfu0NVRIgeHzf+5/LfPlZZr1FqCdZFB8 cPW++/zIonaD1NVfFvxrw4UaKgFDeRhJHjvvaQ+QNwOesUUuY/jVG0F55AcNFcYZgQRluSAmCJy sT/1fZbJPLQHOO226T52Rm/nGTmbD3o5d7yeWbdyU3gwTBZgjSV1m6K5bon2J6iDA2y4A= X-Developer-Key: i=floss@jetm.me; a=openpgp; fpr=9B13B20BCF0EDE23454A93C9B5EEC30BA867771F In-Reply-To: <20260306-agc-proportional-v4-0-e87c7e0d837a@jetm.me> References: <20260306-agc-proportional-v4-0-e87c7e0d837a@jetm.me> 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 + error * 0.04 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 Reviewed-by: Milan Zamazal Tested-by: Barnabás Pőcze --- src/ipa/simple/algorithms/agc.cpp | 65 ++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/src/ipa/simple/algorithms/agc.cpp b/src/ipa/simple/algorithms/agc.cpp index 2f7e040c..ac977d5f 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,66 @@ 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; + 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 factor = 1.0f + static_cast(error) * kExpProportionalGain; + + 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 +112,7 @@ void Agc::updateExposure(IPAContext &context, IPAFrameContext &frameContext, dou LOG(IPASoftExposure, Debug) << "exposureMSV " << exposureMSV + << " error " << error << " factor " << factor << " exp " << exposure << " again " << again; }