{"id":26267,"url":"https://patchwork.libcamera.org/api/1.1/patches/26267/?format=json","web_url":"https://patchwork.libcamera.org/patch/26267/","project":{"id":1,"url":"https://patchwork.libcamera.org/api/1.1/projects/1/?format=json","name":"libcamera","link_name":"libcamera","list_id":"libcamera_core","list_email":"libcamera-devel@lists.libcamera.org","web_url":"","scm_url":"","webscm_url":""},"msgid":"<20260306-agc-proportional-v4-1-e87c7e0d837a@jetm.me>","date":"2026-03-06T18:46:40","name":"[v4,1/3] ipa: simple: agc: Replace bang-bang controller with proportional","commit_ref":null,"pull_url":null,"state":"new","archived":false,"hash":"8e09025b9bdfc16583bc382925ac04380776d389","submitter":{"id":261,"url":"https://patchwork.libcamera.org/api/1.1/people/261/?format=json","name":"Javier Tia","email":"floss@jetm.me"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/26267/mbox/","series":[{"id":5824,"url":"https://patchwork.libcamera.org/api/1.1/series/5824/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=5824","date":"2026-03-06T18:46:39","name":"Simple pipeline: proportional AGC, AWB stats fix, OV2740 black level","version":4,"mbox":"https://patchwork.libcamera.org/series/5824/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/26267/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/26267/checks/","tags":{},"headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 121D2BE086\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri,  6 Mar 2026 18:47:24 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id BE40C62635;\n\tFri,  6 Mar 2026 19:47:23 +0100 (CET)","from fhigh-b6-smtp.messagingengine.com\n\t(fhigh-b6-smtp.messagingengine.com [202.12.124.157])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id BFB4562620\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  6 Mar 2026 19:47:21 +0100 (CET)","from phl-compute-02.internal (phl-compute-02.internal\n\t[10.202.2.42])\n\tby mailfhigh.stl.internal (Postfix) with ESMTP id CD8F57A0155;\n\tFri,  6 Mar 2026 13:47:20 -0500 (EST)","from phl-imap-07 ([10.202.2.97])\n\tby phl-compute-02.internal (MEProxy); Fri, 06 Mar 2026 13:47:20 -0500","by mailuser.phl.internal (Postfix, from userid 501)\n\tid 8B0911EA006B; Fri,  6 Mar 2026 13:47:20 -0500 (EST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=jetm.me header.i=@jetm.me header.b=\"qHKdYu8N\";\n\tdkim=pass (2048-bit key;\n\tunprotected) header.d=messagingengine.com\n\theader.i=@messagingengine.com header.b=\"okr94qrS\"; \n\tdkim-atps=neutral","DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/relaxed; d=jetm.me; h=cc:cc\n\t:content-transfer-encoding:content-type:content-type:date:date\n\t:from:from:in-reply-to:in-reply-to:message-id:mime-version\n\t:references:reply-to:subject:subject:to:to; s=fm3; t=1772822840;\n\tx=1772909240; bh=h+BuvakQdxEZJo6r5m3X1LJOae1UMzkzzyG/TIjsqxo=; b=\n\tqHKdYu8N/vH4pnnS9qnO5IS+fO18gtvFPHHEVcgTq+hAB9YxealTQomaA0ihZzVH\n\tgHi604oark/cxYSwkvRJ3I19wYTnfPXVwmq14LYdGdgxqZufYGMRVL1tRbz6XqtK\n\tthEOE3Lf779mERRuRdIKxSI97UTPJKsCv2BCapKU5veCygsR5uLiY11U50NPcZlA\n\tUSPGjOp0VmtVQ2etQ434iW3t4OlGKSWMzI7HBTP5/UvGY5SZH/Q8Z7GHDDHCvtzD\n\tuetXFD+kI8Fj65OxFtSj7UKOTVRFs3lvjxGRgYEOK5zgwro4DJFKuWjkLKFzyh1E\n\t1I6Yk3tu6j2L+gCOcZQMIg==","v=1; a=rsa-sha256; c=relaxed/relaxed; d=\n\tmessagingengine.com; h=cc:cc:content-transfer-encoding\n\t:content-type:content-type:date:date:feedback-id:feedback-id\n\t:from:from:in-reply-to:in-reply-to:message-id:mime-version\n\t:references:reply-to:subject:subject:to:to:x-me-proxy\n\t:x-me-sender:x-me-sender:x-sasl-enc; s=fm1; t=1772822840; x=\n\t1772909240; bh=h+BuvakQdxEZJo6r5m3X1LJOae1UMzkzzyG/TIjsqxo=; b=o\n\tkr94qrSn/ukFusm0xJjMLu1ycxVqC24Rh2LAQDQNOOpEJvdfbZP/vBrEl0SxErXb\n\tY9Ff4vg0v96uVBjHmX7K44GyZEu18HruazOHRTgaV2NLxrh4d2zxD7ZbhA43L8d/\n\tPzcOq5z7JQorao+BVAxU4jI7MTIV33lG3jF15YH/O4Zq1KlrhI5BtEAAHT9d560i\n\tiOdXDvWbbwEmN+CprAWiv8CRInmD8LN106z1idEBrb4GN7eVG2YzlZW5GZ5zRJqB\n\tv+/5mYm/GWVijvSPWLhlRdCK945gka5wWDxrevk+F0gU8IZWL7/1mutFMeNuhNW0\n\trq+faJO2POhwK34u0guXw=="],"X-ME-Sender":"<xms:OCGradI0GByXu6eXh3itM4vZxVEvzCMN5SH0rQDy5RLy8jbi8CN3Tg>\n\t<xme:OCGraT_96V4gxXWriyDhs1iFPDC2b1K9UianxNT9jWDs2MoPq3wWF81-wKC-dphVt\n\tNGKM5aBvgU5HQNRiY1RvkbDeaqmoCjzSIY88iwk-Crw-e-OK9viB-0>","X-ME-Proxy-Cause":"gggruggvucftvghtrhhoucdtuddrgeefgedrtddtgddvjedttdegucetufdoteggodetrf\n\tdotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu\n\trghilhhouhhtmecufedttdenucesvcftvggtihhpihgvnhhtshculddquddttddmnecujf\n\tgurhepoffhfffugggtgffkvfevofgjfhesthekredtredtjeenucfhrhhomheplfgrvhhi\n\tvghrucfvihgruceofhhlohhsshesjhgvthhmrdhmvgeqnecuggftrfgrthhtvghrnhepve\n\tektdfhffevuddtgfetieevtddvheduhfeggfekveejlefhleefieevueefiefhnecuvehl\n\tuhhsthgvrhfuihiivgeptdenucfrrghrrghmpehmrghilhhfrhhomhepfhhlohhsshesjh\n\tgvthhmrdhmvgdpnhgspghrtghpthhtohepgedpmhhouggvpehsmhhtphhouhhtpdhrtghp\n\tthhtohepsggrrhhnrggsrghsrdhpohgtiigvsehiuggvrghsohhnsghorghrugdrtghomh\n\tdprhgtphhtthhopehflhhoshhssehjvghtmhdrmhgvpdhrtghpthhtoheplhhisggtrghm\n\tvghrrgdquggvvhgvlheslhhishhtshdrlhhisggtrghmvghrrgdrohhrghdprhgtphhtth\n\thopehmiigrmhgriigrlhesrhgvughhrghtrdgtohhm","X-ME-Proxy":"<xmx:OCGraVwC0N1wnPPug7h6rmcUz7_JxGE_OTb6A2rWYPXFKjQesWGxkw>\n\t<xmx:OCGraTG3nyEB34ksQbaSTr2pjJzUt8p13QRa9EdSl5s6JY6_qdA3cw>\n\t<xmx:OCGraZw7BNxJAJ5WGgpxWLxfgxIfZ_wyYFnBRQnZs66-cF887o--ag>\n\t<xmx:OCGraUvXwQVfjK_X69_k997DGwK2Sm2rXG9CmdzA8YaIhbfzhsMMeA>\n\t<xmx:OCGraeGc2Z-0hSYzyd2sJUVEM0lPVYavIRPqgJ4oBccoe6zlqicqhza8>","Feedback-ID":"i9dde48b3:Fastmail","X-Mailer":["MessagingEngine.com Webmail Interface","b4 0.14.3"],"From":"Javier Tia <floss@jetm.me>","Date":"Fri, 06 Mar 2026 12:46:40 -0600","Subject":"[PATCH v4 1/3] ipa: simple: agc: Replace bang-bang controller with\n\tproportional","MIME-Version":"1.0","Content-Type":"text/plain; charset=\"utf-8\"","Content-Transfer-Encoding":"8bit","Message-Id":"<20260306-agc-proportional-v4-1-e87c7e0d837a@jetm.me>","To":"libcamera-devel@lists.libcamera.org","Cc":"Javier Tia <floss@jetm.me>, Milan Zamazal <mzamazal@redhat.com>,\n\t=?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= <barnabas.pocze@ideasonboard.com>","X-Developer-Signature":"v=1; a=openpgp-sha256; l=4996; i=floss@jetm.me;\n\th=from:subject:message-id;\n\tbh=SV7Gcbd6KVxn2JTosswI0D9gL6WG/lCrexuHvmEVNEY=; \n\tb=owEB7QES/pANAwAKAbXuwwuoZ3cfAcsmYgBpqyEPA8AURRa+CBXFQq47ccTyPZN4PlUZOKtSJ\n\tkYBB4wIO+CJAbMEAAEKAB0WIQSbE7ILzw7eI0VKk8m17sMLqGd3HwUCaashDwAKCRC17sMLqGd3\n\tH+/0DAC+haXU0rvymThx+t7rGyihSlVZW7DFrgdq2XRqhbbfbIn/QG/af6eMUKfKGD0GPLpsvsP\n\t+nxosfHz2rCoDfmHSeJSE4ZypVob+zZpl9Nw1Iqoqp78AaDyAaKlSuddAILz2luCaOD4mxGvk6W\n\tmJbDBHidoPY7z650JAcUcBiKYa/V0DzegOFc9BQ6yQhMpNhE8jnv69dEHEZ/PxIsMlbsLPDJjhQ\n\tRSiU9IuzMK9wPx1HdgoFFNML5sY0kHGt2LJ5Uj43jBVWOHYmlVfRy0wp/yQbEsv19853Me/3sO/\n\tddFL+NANLC6zuCEqP5w90PYBx1fBhN5Xe6cjD/SlO9LPfu0NVRIgeHzf+5/LfPlZZr1FqCdZFB8\n\tcPW++/zIonaD1NVfFvxrw4UaKgFDeRhJHjvvaQ+QNwOesUUuY/jVG0F55AcNFcYZgQRluSAmCJy\n\tsT/1fZbJPLQHOO226T52Rm/nGTmbD3o5d7yeWbdyU3gwTBZgjSV1m6K5bon2J6iDA2y4A=","X-Developer-Key":"i=floss@jetm.me; a=openpgp;\n\tfpr=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":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"},"content":"The AGC's updateExposure() uses a fixed ~10% step per frame regardless\nof how far the current exposure is from optimal. With a hysteresis dead\nband of only +/-4%, the controller overshoots when the correct value\nfalls within one step, causing visible brightness oscillation (flicker).\n\nReplace the fixed-step bang-bang controller with a proportional one\nwhere the correction factor scales linearly with the MSV error:\n\n  factor = 1.0 + error * 0.04\n\nAt maximum error (~2.5), this gives the same ~10% step as before. Near\nthe target, steps shrink to <1%, eliminating overshoot. The existing\nhysteresis (kExposureSatisfactory) still prevents hunting on noise.\n\nTested on OV2740 behind Intel IPU6 ISYS (ThinkPad X1 Carbon Gen 10)\nwhere the old controller produced continuous brightness flicker. The\nproportional controller converges in ~3 seconds from cold start with\nno visible oscillation.\n\nSigned-off-by: Javier Tia <floss@jetm.me>\nReviewed-by: Milan Zamazal <mzamazal@redhat.com>\nTested-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>\n---\n src/ipa/simple/algorithms/agc.cpp | 65 ++++++++++++++++++++++++---------------\n 1 file changed, 41 insertions(+), 24 deletions(-)","diff":"diff --git a/src/ipa/simple/algorithms/agc.cpp b/src/ipa/simple/algorithms/agc.cpp\nindex 2f7e040c..ac977d5f 100644\n--- a/src/ipa/simple/algorithms/agc.cpp\n+++ b/src/ipa/simple/algorithms/agc.cpp\n@@ -7,6 +7,8 @@\n \n #include \"agc.h\"\n \n+#include <algorithm>\n+#include <cmath>\n #include <stdint.h>\n \n #include <libcamera/base/log.h>\n@@ -37,52 +39,66 @@ static constexpr float kExposureOptimal = kExposureBinsCount / 2.0;\n  */\n static constexpr float kExposureSatisfactory = 0.2;\n \n+/*\n+ * Proportional gain for exposure/gain adjustment. Maps the MSV error to a\n+ * multiplicative correction factor:\n+ *\n+ *   factor = 1.0 + kExpProportionalGain * error\n+ *\n+ * With kExpProportionalGain = 0.04:\n+ *   - max error ~2.5 -> factor 1.10 (~10% step, same as before)\n+ *   - error 1.0      -> factor 1.04 (~4% step)\n+ *   - error 0.3      -> factor 1.012 (~1.2% step)\n+ *\n+ * This replaces the fixed 10% bang-bang step with a proportional correction\n+ * that converges smoothly and avoids overshooting near the target.\n+ */\n+static constexpr float kExpProportionalGain = 0.04;\n+\n Agc::Agc()\n {\n }\n \n void Agc::updateExposure(IPAContext &context, IPAFrameContext &frameContext, double exposureMSV)\n {\n-\t/*\n-\t * kExpDenominator of 10 gives ~10% increment/decrement;\n-\t * kExpDenominator of 5 - about ~20%\n-\t */\n-\tstatic constexpr uint8_t kExpDenominator = 10;\n-\tstatic constexpr uint8_t kExpNumeratorUp = kExpDenominator + 1;\n-\tstatic constexpr uint8_t kExpNumeratorDown = kExpDenominator - 1;\n-\n \tint32_t &exposure = frameContext.sensor.exposure;\n \tdouble &again = frameContext.sensor.gain;\n \n-\tif (exposureMSV < kExposureOptimal - kExposureSatisfactory) {\n+\tdouble error = kExposureOptimal - exposureMSV;\n+\n+\tif (std::abs(error) <= kExposureSatisfactory)\n+\t\treturn;\n+\n+\t/*\n+\t * Compute a proportional correction factor. The sign of the error\n+\t * determines the direction: positive error means too dark (increase),\n+\t * negative means too bright (decrease).\n+\t */\n+\tfloat factor = 1.0f + static_cast<float>(error) * kExpProportionalGain;\n+\n+\tif (factor > 1.0f) {\n+\t\t/* Scene too dark: increase exposure first, then gain. */\n \t\tif (exposure < context.configuration.agc.exposureMax) {\n-\t\t\tint32_t next = exposure * kExpNumeratorUp / kExpDenominator;\n-\t\t\tif (next - exposure < 1)\n-\t\t\t\texposure += 1;\n-\t\t\telse\n-\t\t\t\texposure = next;\n+\t\t\tint32_t next = static_cast<int32_t>(exposure * factor);\n+\t\t\texposure = std::max(next, exposure + 1);\n \t\t} else {\n-\t\t\tdouble next = again * kExpNumeratorUp / kExpDenominator;\n+\t\t\tdouble next = again * factor;\n \t\t\tif (next - again < context.configuration.agc.againMinStep)\n \t\t\t\tagain += context.configuration.agc.againMinStep;\n \t\t\telse\n \t\t\t\tagain = next;\n \t\t}\n-\t}\n-\n-\tif (exposureMSV > kExposureOptimal + kExposureSatisfactory) {\n+\t} else {\n+\t\t/* Scene too bright: decrease gain first, then exposure. */\n \t\tif (again > context.configuration.agc.again10) {\n-\t\t\tdouble next = again * kExpNumeratorDown / kExpDenominator;\n+\t\t\tdouble next = again * factor;\n \t\t\tif (again - next < context.configuration.agc.againMinStep)\n \t\t\t\tagain -= context.configuration.agc.againMinStep;\n \t\t\telse\n \t\t\t\tagain = next;\n \t\t} else {\n-\t\t\tint32_t next = exposure * kExpNumeratorDown / kExpDenominator;\n-\t\t\tif (exposure - next < 1)\n-\t\t\t\texposure -= 1;\n-\t\t\telse\n-\t\t\t\texposure = next;\n+\t\t\tint32_t next = static_cast<int32_t>(exposure * factor);\n+\t\t\texposure = std::min(next, exposure - 1);\n \t\t}\n \t}\n \n@@ -96,6 +112,7 @@ void Agc::updateExposure(IPAContext &context, IPAFrameContext &frameContext, dou\n \n \tLOG(IPASoftExposure, Debug)\n \t\t<< \"exposureMSV \" << exposureMSV\n+\t\t<< \" error \" << error << \" factor \" << factor\n \t\t<< \" exp \" << exposure << \" again \" << again;\n }\n \n","prefixes":["v4","1/3"]}