Show a patch.

GET /api/patches/19986/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 19986,
    "url": "https://patchwork.libcamera.org/api/patches/19986/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/19986/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/projects/1/?format=api",
        "name": "libcamera",
        "link_name": "libcamera",
        "list_id": "libcamera_core",
        "list_email": "libcamera-devel@lists.libcamera.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20240502133046.1976565-5-dan.scally@ideasonboard.com>",
    "date": "2024-05-02T13:30:42",
    "name": "[v4,4/8] ipa: libipa: Add AgcMeanLuminance base class",
    "commit_ref": null,
    "pull_url": null,
    "state": "accepted",
    "archived": false,
    "hash": "0e0bb4ac92d29827e329b4b35016fdea8d15e473",
    "submitter": {
        "id": 156,
        "url": "https://patchwork.libcamera.org/api/people/156/?format=api",
        "name": "Dan Scally",
        "email": "dan.scally@ideasonboard.com"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/19986/mbox/",
    "series": [
        {
            "id": 4284,
            "url": "https://patchwork.libcamera.org/api/series/4284/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=4284",
            "date": "2024-05-02T13:30:38",
            "name": "Centralise Agc into libipa",
            "version": 4,
            "mbox": "https://patchwork.libcamera.org/series/4284/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/19986/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/19986/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 1E509C328F\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu,  2 May 2024 13:31:27 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 7B1006341F;\n\tThu,  2 May 2024 15:31:21 +0200 (CEST)",
            "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 8521463418\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu,  2 May 2024 15:31:15 +0200 (CEST)",
            "from mail.ideasonboard.com\n\t(cpc141996-chfd3-2-0-cust928.12-3.cable.virginm.net [86.13.91.161])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id DC4C4E45;\n\tThu,  2 May 2024 15:30:17 +0200 (CEST)"
        ],
        "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"uyiidq94\"; dkim-atps=neutral",
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1714656618;\n\tbh=FMTEK/xJl3gKyUJFWA8kpNbDXaczObJmBq4wm8rB/dc=;\n\th=From:To:Cc:Subject:Date:In-Reply-To:References:From;\n\tb=uyiidq94yeIYQm+oBqjZcRDR8MCsTjRQWSWAj1pryVxna5l4EITWtNCGq0G9MkrmP\n\twvMe9WOP3IhOTRlNim94ITJvmVCJuPqVGqKcAZw7sIugl2EzqSoZ0n2BmMonXKtVCZ\n\tLsFDF6hVLdPyNb+roVZC5aO+D7BLBN93UWrzgh9M=",
        "From": "Daniel Scally <dan.scally@ideasonboard.com>",
        "To": "libcamera-devel@lists.libcamera.org",
        "Cc": "Daniel Scally <dan.scally@ideasonboard.com>,\n\tJacopo Mondi <jacopo.mondi@ideasonboard.com>,\n\tStefan Klug <stefan.klug@ideasonboard.com>,\n\tPaul Elder <paul.elder@ideasonboard.com>",
        "Subject": "[PATCH v4 4/8] ipa: libipa: Add AgcMeanLuminance base class",
        "Date": "Thu,  2 May 2024 14:30:42 +0100",
        "Message-Id": "<20240502133046.1976565-5-dan.scally@ideasonboard.com>",
        "X-Mailer": "git-send-email 2.34.1",
        "In-Reply-To": "<20240502133046.1976565-1-dan.scally@ideasonboard.com>",
        "References": "<20240502133046.1976565-1-dan.scally@ideasonboard.com>",
        "MIME-Version": "1.0",
        "Content-Transfer-Encoding": "8bit",
        "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 algorithms for the RkIsp1 and IPU3 IPAs do the same thing in\nvery large part; following the Rpi IPA's algorithm in spirit with a\nfew tunable values in that IPA being hardcoded in the libipa ones.\nAdd a new base class for AgcMeanLuminance which implements the same\nalgorithm and additionally parses yaml tuning files to inform an IPA\nmodule's Agc algorithm about valid constraint and exposure modes and\ntheir associated bounds.\n\nReviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>\nReviewed-by: Stefan Klug <stefan.klug@ideasonboard.com>\nReviewed-by: Paul Elder <paul.elder@ideasonboard.com>\nSigned-off-by: Daniel Scally <dan.scally@ideasonboard.com>\n---\nChanges in v4:\n\n\t- Renamed configureExposureModeHelpers() to setLimits()\n\t- Fixed a problem that prevented digital gain being used by clamping the\n\t  target exposure to maximum shutter time and _analogue_ gain.\n\t- Some minor style and comment changes\n\nChanges in v3:\n\n\t- Finished renaming the class in all references to it (I hope...)\n\t- Updated documentation to try to be a bit clearer about how to use\n\t  the base class\n\t- Made functions const/private where possible\nChanges in v2:\n\n\t- Renamed the class and files\n\t- Expanded the documentation\n\t- Added parseTuningData() so derived classes can call a single function\n\t  to cover all the parsing in ::init().\n\n src/ipa/libipa/agc_mean_luminance.cpp | 577 ++++++++++++++++++++++++++\n src/ipa/libipa/agc_mean_luminance.h   |  96 +++++\n src/ipa/libipa/meson.build            |   2 +\n 3 files changed, 675 insertions(+)\n create mode 100644 src/ipa/libipa/agc_mean_luminance.cpp\n create mode 100644 src/ipa/libipa/agc_mean_luminance.h",
    "diff": "diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp\nnew file mode 100644\nindex 00000000..004d84d8\n--- /dev/null\n+++ b/src/ipa/libipa/agc_mean_luminance.cpp\n@@ -0,0 +1,577 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024 Ideas on Board Oy\n+ *\n+ * agc_mean_luminance.cpp - Base class for mean luminance AGC algorithms\n+ */\n+\n+#include \"agc_mean_luminance.h\"\n+\n+#include <cmath>\n+\n+#include <libcamera/base/log.h>\n+#include <libcamera/control_ids.h>\n+\n+#include \"exposure_mode_helper.h\"\n+\n+using namespace libcamera::controls;\n+\n+/**\n+ * \\file agc_mean_luminance.h\n+ * \\brief Base class implementing mean luminance AEGC\n+ */\n+\n+namespace libcamera {\n+\n+using namespace std::literals::chrono_literals;\n+\n+LOG_DEFINE_CATEGORY(AgcMeanLuminance)\n+\n+namespace ipa {\n+\n+/*\n+ * Number of frames for which to run the algorithm at full speed, before slowing\n+ * down to prevent large and jarring changes in exposure from frame to frame.\n+ */\n+static constexpr uint32_t kNumStartupFrames = 10;\n+\n+/*\n+ * Default relative luminance target\n+ *\n+ * This value should be chosen so that when the camera points at a grey target,\n+ * the resulting image brightness looks \"right\". Custom values can be passed\n+ * as the relativeLuminanceTarget value in sensor tuning files.\n+ */\n+static constexpr double kDefaultRelativeLuminanceTarget = 0.16;\n+\n+/**\n+ * \\struct AgcMeanLuminance::AgcConstraint\n+ * \\brief The boundaries and target for an AeConstraintMode constraint\n+ *\n+ * This structure describes an AeConstraintMode constraint for the purposes of\n+ * this algorithm. These constraints are expressed as a pair of quantile\n+ * boundaries for a histogram, along with a luminance target and a bounds-type.\n+ * The algorithm uses the constraints by ensuring that the defined portion of a\n+ * luminance histogram (I.E. lying between the two quantiles) is above or below\n+ * the given luminance value.\n+ */\n+\n+/**\n+ * \\enum AgcMeanLuminance::AgcConstraint::Bound\n+ * \\brief Specify whether the constraint defines a lower or upper bound\n+ * \\var AgcMeanLuminance::AgcConstraint::lower\n+ * \\brief The constraint defines a lower bound\n+ * \\var AgcMeanLuminance::AgcConstraint::upper\n+ * \\brief The constraint defines an upper bound\n+ */\n+\n+/**\n+ * \\var AgcMeanLuminance::AgcConstraint::bound\n+ * \\brief The type of constraint bound\n+ */\n+\n+/**\n+ * \\var AgcMeanLuminance::AgcConstraint::qLo\n+ * \\brief The lower quantile to use for the constraint\n+ */\n+\n+/**\n+ * \\var AgcMeanLuminance::AgcConstraint::qHi\n+ * \\brief The upper quantile to use for the constraint\n+ */\n+\n+/**\n+ * \\var AgcMeanLuminance::AgcConstraint::yTarget\n+ * \\brief The luminance target for the constraint\n+ */\n+\n+/**\n+ * \\class AgcMeanLuminance\n+ * \\brief A mean-based auto-exposure algorithm\n+ *\n+ * This algorithm calculates a shutter time, analogue and digital gain such that\n+ * the normalised mean luminance value of an image is driven towards a target,\n+ * which itself is discovered from tuning data. The algorithm is a two-stage\n+ * process.\n+ *\n+ * In the first stage, an initial gain value is derived by iteratively comparing\n+ * the gain-adjusted mean luminance across the entire image against a target,\n+ * and selecting a value which pushes it as closely as possible towards the\n+ * target.\n+ *\n+ * In the second stage we calculate the gain required to drive the average of a\n+ * section of a histogram to a target value, where the target and the boundaries\n+ * of the section of the histogram used in the calculation are taken from the\n+ * values defined for the currently configured AeConstraintMode within the\n+ * tuning data. This class provides a helper function to parse those tuning data\n+ * to discover the constraints, and so requires a specific format for those\n+ * data which is described in \\ref parseTuningData(). The gain from the first\n+ * stage is then clamped to the gain from this stage.\n+ *\n+ * The final gain is used to adjust the effective exposure value of the image,\n+ * and that new exposure value is divided into shutter time, analogue gain and\n+ * digital gain according to the selected AeExposureMode. This class uses the\n+ * \\ref ExposureModeHelper class to assist in that division, and expects the\n+ * data needed to initialise that class to be present in tuning data in a\n+ * format described in \\ref parseTuningData().\n+ *\n+ * In order to be able to use this algorithm an IPA module needs to be able to\n+ * do the following:\n+ *\n+ * 1. Provide a luminance estimation across an entire image.\n+ * 2. Provide a luminance Histogram for the image to use in calculating\n+ *    constraint compliance. The precision of the Histogram that is available\n+ *    will determine the supportable precision of the constraints.\n+ *\n+ * IPA modules that want to use this class to implement their AEGC algorithm\n+ * should derive it and provide an overriding estimateLuminance() function for\n+ * this class to use. They must call parseTuningData() in init(), and must also\n+ * call setLimits() and resetFrameCounter() in configure(). They may then use\n+ * calculateNewEv() in process(). If the limits passed to setLimits() change for\n+ * any reason (for example, in response to a FrameDurationLimit control being\n+ * passed in queueRequest()) then setLimits() must be called again with the new\n+ * values.\n+ */\n+\n+AgcMeanLuminance::AgcMeanLuminance()\n+\t: frameCount_(0), filteredExposure_(0s), relativeLuminanceTarget_(0)\n+{\n+}\n+\n+AgcMeanLuminance::~AgcMeanLuminance() = default;\n+\n+void AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)\n+{\n+\trelativeLuminanceTarget_ =\n+\t\ttuningData[\"relativeLuminanceTarget\"].get<double>(kDefaultRelativeLuminanceTarget);\n+}\n+\n+void AgcMeanLuminance::parseConstraint(const YamlObject &modeDict, int32_t id)\n+{\n+\tfor (const auto &[boundName, content] : modeDict.asDict()) {\n+\t\tif (boundName != \"upper\" && boundName != \"lower\") {\n+\t\t\tLOG(AgcMeanLuminance, Warning)\n+\t\t\t\t<< \"Ignoring unknown constraint bound '\" << boundName << \"'\";\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tunsigned int idx = static_cast<unsigned int>(boundName == \"upper\");\n+\t\tAgcConstraint::Bound bound = static_cast<AgcConstraint::Bound>(idx);\n+\t\tdouble qLo = content[\"qLo\"].get<double>().value_or(0.98);\n+\t\tdouble qHi = content[\"qHi\"].get<double>().value_or(1.0);\n+\t\tdouble yTarget =\n+\t\t\tcontent[\"yTarget\"].getList<double>().value_or(std::vector<double>{ 0.5 }).at(0);\n+\n+\t\tAgcConstraint constraint = { bound, qLo, qHi, yTarget };\n+\n+\t\tif (!constraintModes_.count(id))\n+\t\t\tconstraintModes_[id] = {};\n+\n+\t\tif (idx)\n+\t\t\tconstraintModes_[id].push_back(constraint);\n+\t\telse\n+\t\t\tconstraintModes_[id].insert(constraintModes_[id].begin(), constraint);\n+\t}\n+}\n+\n+int AgcMeanLuminance::parseConstraintModes(const YamlObject &tuningData)\n+{\n+\tstd::vector<ControlValue> availableConstraintModes;\n+\n+\tconst YamlObject &yamlConstraintModes = tuningData[controls::AeConstraintMode.name()];\n+\tif (yamlConstraintModes.isDictionary()) {\n+\t\tfor (const auto &[modeName, modeDict] : yamlConstraintModes.asDict()) {\n+\t\t\tif (AeConstraintModeNameValueMap.find(modeName) ==\n+\t\t\t    AeConstraintModeNameValueMap.end()) {\n+\t\t\t\tLOG(AgcMeanLuminance, Warning)\n+\t\t\t\t\t<< \"Skipping unknown constraint mode '\" << modeName << \"'\";\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif (!modeDict.isDictionary()) {\n+\t\t\t\tLOG(AgcMeanLuminance, Error)\n+\t\t\t\t\t<< \"Invalid constraint mode '\" << modeName << \"'\";\n+\t\t\t\treturn -EINVAL;\n+\t\t\t}\n+\n+\t\t\tparseConstraint(modeDict,\n+\t\t\t\t\tAeConstraintModeNameValueMap.at(modeName));\n+\t\t\tavailableConstraintModes.push_back(\n+\t\t\t\tAeConstraintModeNameValueMap.at(modeName));\n+\t\t}\n+\t}\n+\n+\t/*\n+\t * If the tuning data file contains no constraints then we use the\n+\t * default constraint that the IPU3/RkISP1 Agc algorithms were adhering\n+\t * to anyway before centralisation; this constraint forces the top 2% of\n+\t * the histogram to be at least 0.5.\n+\t */\n+\tif (constraintModes_.empty()) {\n+\t\tAgcConstraint constraint = {\n+\t\t\tAgcConstraint::Bound::lower,\n+\t\t\t0.98,\n+\t\t\t1.0,\n+\t\t\t0.5\n+\t\t};\n+\n+\t\tconstraintModes_[controls::ConstraintNormal].insert(\n+\t\t\tconstraintModes_[controls::ConstraintNormal].begin(),\n+\t\t\tconstraint);\n+\t\tavailableConstraintModes.push_back(\n+\t\t\tAeConstraintModeNameValueMap.at(\"ConstraintNormal\"));\n+\t}\n+\n+\tcontrols_[&controls::AeConstraintMode] = ControlInfo(availableConstraintModes);\n+\n+\treturn 0;\n+}\n+\n+int AgcMeanLuminance::parseExposureModes(const YamlObject &tuningData)\n+{\n+\tstd::vector<ControlValue> availableExposureModes;\n+\n+\tconst YamlObject &yamlExposureModes = tuningData[controls::AeExposureMode.name()];\n+\tif (yamlExposureModes.isDictionary()) {\n+\t\tfor (const auto &[modeName, modeValues] : yamlExposureModes.asDict()) {\n+\t\t\tif (AeExposureModeNameValueMap.find(modeName) ==\n+\t\t\t    AeExposureModeNameValueMap.end()) {\n+\t\t\t\tLOG(AgcMeanLuminance, Warning)\n+\t\t\t\t\t<< \"Skipping unknown exposure mode '\" << modeName << \"'\";\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\n+\t\t\tif (!modeValues.isDictionary()) {\n+\t\t\t\tLOG(AgcMeanLuminance, Error)\n+\t\t\t\t\t<< \"Invalid exposure mode '\" << modeName << \"'\";\n+\t\t\t\treturn -EINVAL;\n+\t\t\t}\n+\n+\t\t\tstd::vector<uint32_t> shutters =\n+\t\t\t\tmodeValues[\"shutter\"].getList<uint32_t>().value_or(std::vector<uint32_t>{});\n+\t\t\tstd::vector<double> gains =\n+\t\t\t\tmodeValues[\"gain\"].getList<double>().value_or(std::vector<double>{});\n+\n+\t\t\tif (shutters.size() != gains.size()) {\n+\t\t\t\tLOG(AgcMeanLuminance, Error)\n+\t\t\t\t\t<< \"Shutter and gain array sizes unequal\";\n+\t\t\t\treturn -EINVAL;\n+\t\t\t}\n+\n+\t\t\tif (shutters.empty()) {\n+\t\t\t\tLOG(AgcMeanLuminance, Error)\n+\t\t\t\t\t<< \"Shutter and gain arrays are empty\";\n+\t\t\t\treturn -EINVAL;\n+\t\t\t}\n+\n+\t\t\tstd::vector<std::pair<utils::Duration, double>> stages;\n+\t\t\tfor (unsigned int i = 0; i < shutters.size(); i++) {\n+\t\t\t\tstages.push_back({\n+\t\t\t\t\tstd::chrono::microseconds(shutters[i]),\n+\t\t\t\t\tgains[i]\n+\t\t\t\t});\n+\t\t\t}\n+\n+\t\t\tstd::shared_ptr<ExposureModeHelper> helper =\n+\t\t\t\tstd::make_shared<ExposureModeHelper>(stages);\n+\n+\t\t\texposureModeHelpers_[AeExposureModeNameValueMap.at(modeName)] = helper;\n+\t\t\tavailableExposureModes.push_back(AeExposureModeNameValueMap.at(modeName));\n+\t\t}\n+\t}\n+\n+\t/*\n+\t * If we don't have any exposure modes in the tuning data we create an\n+\t * ExposureModeHelper using an empty vector of stages. This will result\n+\t * in the ExposureModeHelper simply driving the shutter as high as\n+\t * possible before touching gain.\n+\t */\n+\tif (availableExposureModes.empty()) {\n+\t\tint32_t exposureModeId = AeExposureModeNameValueMap.at(\"ExposureNormal\");\n+\t\tstd::vector<std::pair<utils::Duration, double>> stages = { };\n+\n+\t\tstd::shared_ptr<ExposureModeHelper> helper =\n+\t\t\tstd::make_shared<ExposureModeHelper>(stages);\n+\n+\t\texposureModeHelpers_[exposureModeId] = helper;\n+\t\tavailableExposureModes.push_back(exposureModeId);\n+\t}\n+\n+\tcontrols_[&controls::AeExposureMode] = ControlInfo(availableExposureModes);\n+\n+\treturn 0;\n+}\n+\n+/**\n+ * \\brief Parse tuning data for AeConstraintMode and AeExposureMode controls\n+ * \\param[in] tuningData the YamlObject representing the tuning data\n+ *\n+ * This function parses tuning data to build the list of allowed values for the\n+ * AeConstraintMode and AeExposureMode controls. Those tuning data must provide\n+ * the data in a specific format; the Agc algorithm's tuning data should contain\n+ * a dictionary called AeConstraintMode containing per-mode setting dictionaries\n+ * with the key being a value from \\ref controls::AeConstraintModeNameValueMap.\n+ * Each mode dict may contain either a \"lower\" or \"upper\" key or both, for\n+ * example:\n+ *\n+ * \\code{.unparsed}\n+ * algorithms:\n+ *   - Agc:\n+ *       AeConstraintMode:\n+ *         ConstraintNormal:\n+ *           lower:\n+ *             qLo: 0.98\n+ *             qHi: 1.0\n+ *             yTarget: 0.5\n+ *         ConstraintHighlight:\n+ *           lower:\n+ *             qLo: 0.98\n+ *             qHi: 1.0\n+ *             yTarget: 0.5\n+ *           upper:\n+ *             qLo: 0.98\n+ *             qHi: 1.0\n+ *             yTarget: 0.8\n+ *\n+ * \\endcode\n+ *\n+ * For the AeExposureMode control the data should contain a dictionary called\n+ * AeExposureMode containing per-mode setting dictionaries with the key being a\n+ * value from \\ref controls::AeExposureModeNameValueMap. Each mode dict should\n+ * contain an array of shutter times with the key \"shutter\" and an array of gain\n+ * values with the key \"gain\", in this format:\n+ *\n+ * \\code{.unparsed}\n+ * algorithms:\n+ *   - Agc:\n+ *       AeExposureMode:\n+ *         ExposureNormal:\n+ *           shutter: [ 100, 10000, 30000, 60000, 120000 ]\n+ *           gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ]\n+ *         ExposureShort:\n+ *           shutter: [ 100, 10000, 30000, 60000, 120000 ]\n+ *           gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ]\n+ *\n+ * \\endcode\n+ *\n+ * \\return 0 on success or a negative error code\n+ */\n+int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)\n+{\n+\tint ret;\n+\n+\tparseRelativeLuminanceTarget(tuningData);\n+\n+\tret = parseConstraintModes(tuningData);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\treturn parseExposureModes(tuningData);\n+}\n+\n+/**\n+ * \\brief Set the ExposureModeHelper limits for this class\n+ * \\param[in] minShutter Minimum shutter time to allow\n+ * \\param[in] maxShutter Maximum shutter time to allow\n+ * \\param[in] minGain Minimum gain to allow\n+ * \\param[in] maxGain Maximum gain to allow\n+ *\n+ * This function calls \\ref ExposureModeHelper::setLimits() for each\n+ * ExposureModeHelper that has been created for this class.\n+ */\n+void AgcMeanLuminance::setLimits(utils::Duration minShutter,\n+\t\t\t\t utils::Duration maxShutter,\n+\t\t\t\t double minGain, double maxGain)\n+{\n+\tfor (auto &[id, helper] : exposureModeHelpers_)\n+\t\thelper->setLimits(minShutter, maxShutter, minGain, maxGain);\n+}\n+\n+/**\n+ * \\fn AgcMeanLuminance::constraintModes()\n+ * \\brief Get the constraint modes that have been parsed from tuning data\n+ */\n+\n+/**\n+ * \\fn AgcMeanLuminance::exposureModeHelpers()\n+ * \\brief Get the ExposureModeHelpers that have been parsed from tuning data\n+ */\n+\n+/**\n+ * \\fn AgcMeanLuminance::controls()\n+ * \\brief Get the controls that have been generated after parsing tuning data\n+ */\n+\n+/**\n+ * \\fn AgcMeanLuminance::estimateLuminance(const double gain)\n+ * \\brief Estimate the luminance of an image, adjusted by a given gain\n+ * \\param[in] gain The gain with which to adjust the luminance estimate\n+ *\n+ * This function estimates the average relative luminance of the frame that\n+ * would be output by the sensor if an additional \\a gain was applied. It is a\n+ * pure virtual function because estimation of luminance is a hardware-specific\n+ * operation, which depends wholly on the format of the stats that are delivered\n+ * to libcamera from the ISP. Derived classes must override this function with\n+ * one that calculates the normalised mean luminance value across the entire\n+ * image.\n+ *\n+ * \\return The normalised relative luminance of the image\n+ */\n+\n+/**\n+ * \\brief Estimate the initial gain needed to achieve a relative luminance\n+ * target\n+ * \\return The calculated initial gain\n+ */\n+double AgcMeanLuminance::estimateInitialGain() const\n+{\n+\tdouble yTarget = relativeLuminanceTarget_;\n+\tdouble yGain = 1.0;\n+\n+\t/*\n+\t* To account for non-linearity caused by saturation, the value needs to\n+\t* be estimated in an iterative process, as multiplying by a gain will\n+\t* not increase the relative luminance by the same factor if some image\n+\t* regions are saturated.\n+\t*/\n+\tfor (unsigned int i = 0; i < 8; i++) {\n+\t\tdouble yValue = estimateLuminance(yGain);\n+\t\tdouble extra_gain = std::min(10.0, yTarget / (yValue + .001));\n+\n+\t\tyGain *= extra_gain;\n+\t\tLOG(AgcMeanLuminance, Debug) << \"Y value: \" << yValue\n+\t\t\t\t<< \", Y target: \" << yTarget\n+\t\t\t\t<< \", gives gain \" << yGain;\n+\n+\t\tif (utils::abs_diff(extra_gain, 1.0) < 0.01)\n+\t\t\tbreak;\n+\t}\n+\n+\treturn yGain;\n+}\n+\n+/**\n+ * \\brief Clamp gain within the bounds of a defined constraint\n+ * \\param[in] constraintModeIndex The index of the constraint to adhere to\n+ * \\param[in] hist A histogram over which to calculate inter-quantile means\n+ * \\param[in] gain The gain to clamp\n+ *\n+ * \\return The gain clamped within the constraint bounds\n+ */\n+double AgcMeanLuminance::constraintClampGain(uint32_t constraintModeIndex,\n+\t\t\t\t\t     const Histogram &hist,\n+\t\t\t\t\t     double gain)\n+{\n+\tstd::vector<AgcConstraint> &constraints = constraintModes_[constraintModeIndex];\n+\tfor (const AgcConstraint &constraint : constraints) {\n+\t\tdouble newGain = constraint.yTarget * hist.bins() /\n+\t\t\t\t hist.interQuantileMean(constraint.qLo, constraint.qHi);\n+\n+\t\tif (constraint.bound == AgcConstraint::Bound::lower &&\n+\t\t    newGain > gain)\n+\t\t\tgain = newGain;\n+\n+\t\tif (constraint.bound == AgcConstraint::Bound::upper &&\n+\t\t    newGain < gain)\n+\t\t\tgain = newGain;\n+\t}\n+\n+\treturn gain;\n+}\n+\n+/**\n+ * \\brief Apply a filter on the exposure value to limit the speed of changes\n+ * \\param[in] exposureValue The target exposure from the AGC algorithm\n+ *\n+ * The speed of the filter is adaptive, and will produce the target quicker\n+ * during startup, or when the target exposure is within 20% of the most recent\n+ * filter output.\n+ *\n+ * \\return The filtered exposure\n+ */\n+utils::Duration AgcMeanLuminance::filterExposure(utils::Duration exposureValue)\n+{\n+\tdouble speed = 0.2;\n+\n+\t/* Adapt instantly if we are in startup phase. */\n+\tif (frameCount_ < kNumStartupFrames)\n+\t\tspeed = 1.0;\n+\n+\t/*\n+\t * If we are close to the desired result, go faster to avoid making\n+\t * multiple micro-adjustments.\n+\t * \\todo Make this customisable?\n+\t */\n+\tif (filteredExposure_ < 1.2 * exposureValue &&\n+\t    filteredExposure_ > 0.8 * exposureValue)\n+\t\tspeed = sqrt(speed);\n+\n+\tfilteredExposure_ = speed * exposureValue +\n+\t\t\t    filteredExposure_ * (1.0 - speed);\n+\n+\treturn filteredExposure_;\n+}\n+\n+/**\n+ * \\brief Calculate the new exposure value and splut it between shutter time and gain\n+ * \\param[in] constraintModeIndex The index of the current constraint mode\n+ * \\param[in] exposureModeIndex The index of the current exposure mode\n+ * \\param[in] yHist A Histogram from the ISP statistics to use in constraining\n+ * the calculated gain\n+ * \\param[in] effectiveExposureValue The EV applied to the frame from which the\n+ * statistics in use derive\n+ *\n+ * Calculate a new exposure value to try to obtain the target. The calculated\n+ * exposure value is filtered to prevent rapid changes from frame to frame, and\n+ * divided into shutter time, analogue and digital gain.\n+ *\n+ * \\return Tuple of shutter time, analogue gain, and digital gain\n+ */\n+std::tuple<utils::Duration, double, double>\n+AgcMeanLuminance::calculateNewEv(uint32_t constraintModeIndex,\n+\t\t\t\t uint32_t exposureModeIndex,\n+\t\t\t\t const Histogram &yHist,\n+\t\t\t\t utils::Duration effectiveExposureValue)\n+{\n+\t/*\n+\t * The pipeline handler should validate that we have received an allowed\n+\t * value for AeExposureMode.\n+\t */\n+\tstd::shared_ptr<ExposureModeHelper> exposureModeHelper =\n+\t\texposureModeHelpers_.at(exposureModeIndex);\n+\n+\tdouble gain = estimateInitialGain();\n+\tgain = constraintClampGain(constraintModeIndex, yHist, gain);\n+\n+\t/*\n+\t * We don't check whether we're already close to the target, because\n+\t * even if the effective exposure value is the same as the last frame's\n+\t * we could have switched to an exposure mode that would require a new\n+\t * pass through the splitExposure() function.\n+\t */\n+\n+\tutils::Duration newExposureValue = effectiveExposureValue * gain;\n+\n+\t/*\n+\t * We filter the exposure value to make sure changes are not too jarring\n+\t * from frame to frame.\n+\t */\n+\tnewExposureValue = filterExposure(newExposureValue);\n+\n+\tframeCount_++;\n+\treturn exposureModeHelper->splitExposure(newExposureValue);\n+}\n+\n+/**\n+ * \\fn AgcMeanLuminance::resetFrameCount()\n+ * \\brief Reset the frame counter\n+ *\n+ * This function resets the internal frame counter, which exists to help the\n+ * algorithm decide whether it should respond instantly or not. The expectation\n+ * is for derived classes to call this function before each camera start call in\n+ * their configure() function.\n+ */\n+\n+}; /* namespace ipa */\n+\n+}; /* namespace libcamera */\ndiff --git a/src/ipa/libipa/agc_mean_luminance.h b/src/ipa/libipa/agc_mean_luminance.h\nnew file mode 100644\nindex 00000000..ef8ba28f\n--- /dev/null\n+++ b/src/ipa/libipa/agc_mean_luminance.h\n@@ -0,0 +1,96 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024 Ideas on Board Oy\n+ *\n+ agc_mean_luminance.h - Base class for mean luminance AGC algorithms\n+ */\n+\n+#pragma once\n+\n+#include <map>\n+#include <memory>\n+#include <tuple>\n+#include <vector>\n+\n+#include <libcamera/controls.h>\n+\n+#include \"libcamera/internal/yaml_parser.h\"\n+\n+#include \"exposure_mode_helper.h\"\n+#include \"histogram.h\"\n+\n+namespace libcamera {\n+\n+namespace ipa {\n+\n+class AgcMeanLuminance\n+{\n+public:\n+\tAgcMeanLuminance();\n+\tvirtual ~AgcMeanLuminance();\n+\n+\tstruct AgcConstraint {\n+\t\tenum class Bound {\n+\t\t\tlower = 0,\n+\t\t\tupper = 1\n+\t\t};\n+\t\tBound bound;\n+\t\tdouble qLo;\n+\t\tdouble qHi;\n+\t\tdouble yTarget;\n+\t};\n+\n+\tint parseTuningData(const YamlObject &tuningData);\n+\n+\tvoid setLimits(utils::Duration minShutter, utils::Duration maxShutter,\n+\t\t       double minGain, double maxGain);\n+\n+\tstd::map<int32_t, std::vector<AgcConstraint>> constraintModes()\n+\t{\n+\t\treturn constraintModes_;\n+\t}\n+\n+\tstd::map<int32_t, std::shared_ptr<ExposureModeHelper>> exposureModeHelpers()\n+\t{\n+\t\treturn exposureModeHelpers_;\n+\t}\n+\n+\tControlInfoMap::Map controls()\n+\t{\n+\t\treturn controls_;\n+\t}\n+\n+\tstd::tuple<utils::Duration, double, double>\n+\tcalculateNewEv(uint32_t constraintModeIndex, uint32_t exposureModeIndex,\n+\t\t       const Histogram &yHist, utils::Duration effectiveExposureValue);\n+\n+\tvoid resetFrameCount()\n+\t{\n+\t\tframeCount_ = 0;\n+\t}\n+\n+private:\n+\tvirtual double estimateLuminance(const double gain) const = 0;\n+\n+\tvoid parseRelativeLuminanceTarget(const YamlObject &tuningData);\n+\tvoid parseConstraint(const YamlObject &modeDict, int32_t id);\n+\tint parseConstraintModes(const YamlObject &tuningData);\n+\tint parseExposureModes(const YamlObject &tuningData);\n+\tdouble estimateInitialGain() const;\n+\tdouble constraintClampGain(uint32_t constraintModeIndex,\n+\t\t\t\t   const Histogram &hist,\n+\t\t\t\t   double gain);\n+\tutils::Duration filterExposure(utils::Duration exposureValue);\n+\n+\tuint64_t frameCount_;\n+\tutils::Duration filteredExposure_;\n+\tdouble relativeLuminanceTarget_;\n+\n+\tstd::map<int32_t, std::vector<AgcConstraint>> constraintModes_;\n+\tstd::map<int32_t, std::shared_ptr<ExposureModeHelper>> exposureModeHelpers_;\n+\tControlInfoMap::Map controls_;\n+};\n+\n+}; /* namespace ipa */\n+\n+}; /* namespace libcamera */\ndiff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build\nindex 37fbd177..7ce885da 100644\n--- a/src/ipa/libipa/meson.build\n+++ b/src/ipa/libipa/meson.build\n@@ -1,6 +1,7 @@\n # SPDX-License-Identifier: CC0-1.0\n \n libipa_headers = files([\n+    'agc_mean_luminance.h',\n     'algorithm.h',\n     'camera_sensor_helper.h',\n     'exposure_mode_helper.h',\n@@ -10,6 +11,7 @@ libipa_headers = files([\n ])\n \n libipa_sources = files([\n+    'agc_mean_luminance.cpp',\n     'algorithm.cpp',\n     'camera_sensor_helper.cpp',\n     'exposure_mode_helper.cpp',\n",
    "prefixes": [
        "v4",
        "4/8"
    ]
}