From patchwork Wed May 6 23:07:17 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: devve X-Patchwork-Id: 26662 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 BEC95C3302 for ; Wed, 6 May 2026 23:07:36 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 0DC83630A2; Thu, 7 May 2026 01:07:35 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="Bss7U651"; dkim-atps=neutral Received: from mail-qk1-x732.google.com (mail-qk1-x732.google.com [IPv6:2607:f8b0:4864:20::732]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 81AA663024 for ; Thu, 7 May 2026 01:07:32 +0200 (CEST) Received: by mail-qk1-x732.google.com with SMTP id af79cd13be357-8cb5c9ba82bso35301085a.2 for ; Wed, 06 May 2026 16:07:32 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778108851; x=1778713651; darn=lists.libcamera.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to; bh=dZKZKTCGEv+ghBeM3aBja4SAmlWxPm+UsMadpqZ8y88=; b=Bss7U651ES8BR2TAsK9IFD3L63Hi+kNbOknapBlmcSRxojqI0nVpgjuGxYMUckclI/ k8+7zH2Y6nUiBORJQclnoey7zcYBs2ky0a9ygfhPuCIVXBsxORPya0q6JZqg3/4+M9mj wjAUm+PxtzsOGaJusAuDLylLxD5k4Tu9qKicuu5MYd+Mk7ofaOKCgc8pXgpQ7qc2nBhm L4KVrnTGEP7QDqq/aZJjfbc5xY1FFXeq0POxk6cR6Df6Dq01i7XiwVmi8rVZtQ5KakIy ACptZd4TnzpOgr5/h016f8k3ONUHBTGvCtWhIFfMNilXe7HK4V9oDH2slVIQuOosx/0+ 5DyA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778108851; x=1778713651; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to; bh=dZKZKTCGEv+ghBeM3aBja4SAmlWxPm+UsMadpqZ8y88=; b=HhxVHqzDpGJgqzwq5RsRvMZCx8Khcvfl0E3HjJNpLId4JD0qLwHQ7TNQe4KNZi5cy0 4R5h22LgQkrS6DFV731+zlFVzdbVH6or3DybakKA+R4eQ9U+ONbtq8JZn5KrdxqtGPBu hh2IKinDh6OhVT2GG7rgCgZaFauy+ns0uzw6eWMhg7OCQGpP6Z6qjezSgBnoCqJvPU48 GSVD7C2WTb4gz0VA4mZTkIFJcNAGB382TqEGOkHHdwbbmMwSnGha3zUCAP0VmKv6PFIC xjb4EfzXxtt/BaDF37lAR0SD/R/9XKnEu8rU3KIJ/s+B1E7dg+oJzKzFTbtF+uRb4CZb FBbw== X-Gm-Message-State: AOJu0YwyoOBy8gLRVYDR3vuzSGg1zFHzPftypWNlncunNpbNxWqFA048 iJxzjpYhjhKEFMVyq96cc41+kP3yGn1Av4WVNgB0iIUZz/f8txH8+42HTQdM/w== X-Gm-Gg: AeBDietTPOvH2i0b8MMGIqp2NDH+Uxpnzh3VMr8VOZ4cuP0t4b+pHrVrApvE05vC5Gb DbxXfCtWFi+biayHkMkXYaSKPbTQ9jhJQRaWJRVQApd3UdRc3tk25pbTXVc/YtsHXU0ophdQ77m CsIc1CEBYgCoABi2/quU7Mk0MXV4k2YB7MWglLDkKvT2ppA6JYVIpodFnEswcc4UdPyqSTNfBy2 /EYb3gUnvhNAMpGaebL6Ej0R9o+zDECzueD7CXfVzV16tZ+JlnD/TJO3NKxcFOpTzu++bIcoFN+ UmiPhc50MjnVrOXxwjaoMGxxbPCFjhpOQqdT9LvJtRIGGLn2XVPWUXEtRkdNlv+6FNYzyCniIgp yu1t8BW2loZziF7E0hRZrsXqz9qee01syM9SYvxBuqHifhF53KDUSmAxwChpn3OsBJEdAJi0TUy Ug6Thx6cNH6fCPMy1AboBkY8+1c6LgTxcyS8EoXwm8c2RTugwYCyFOu4HBqCroMgmRSIlJIznMR rSR/h309T9ovCW7O3jdudOU/M4fkFaJ X-Received: by 2002:a05:620a:4451:b0:8fb:e57e:9e0b with SMTP id af79cd13be357-904d69d8df5mr819540385a.46.1778108851410; Wed, 06 May 2026 16:07:31 -0700 (PDT) Received: from dexps.speedport.ip (p200300eda74453cf3cf3f7929e513b94.dip0.t-ipconnect.de. [2003:ed:a744:53cf:3cf3:f792:9e51:3b94]) by smtp.gmail.com with ESMTPSA id af79cd13be357-8fc2c25324esm2035266385a.23.2026.05.06.16.07.30 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 06 May 2026 16:07:31 -0700 (PDT) From: d3vv3 To: libcamera-devel@lists.libcamera.org Subject: [PATCH v2 05/10] ipa: simple: agc: Replace bang-bang controller with proportional Date: Thu, 7 May 2026 01:07:17 +0200 Message-ID: <20260506230722.1041596-6-devve.3@gmail.com> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260506230722.1041596-1-devve.3@gmail.com> References: <20260506230722.1041596-1-devve.3@gmail.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" From: Javier Tia 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 Signed-off-by: d3vv3 --- 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; }