Patch Detail
Show a patch.
GET /api/patches/19896/?format=api
{ "id": 19896, "url": "https://patchwork.libcamera.org/api/patches/19896/?format=api", "web_url": "https://patchwork.libcamera.org/patch/19896/", "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": "<20240417131536.484129-5-dan.scally@ideasonboard.com>", "date": "2024-04-17T13:15:32", "name": "[v2,4/8] ipa: libipa: Add MeanLuminanceAgc base class", "commit_ref": null, "pull_url": null, "state": "superseded", "archived": false, "hash": "6917955a9947aa3e9a7f991e59fef6c3438e1fc6", "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/19896/mbox/", "series": [ { "id": 4260, "url": "https://patchwork.libcamera.org/api/series/4260/?format=api", "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=4260", "date": "2024-04-17T13:15:28", "name": "Centralise Agc into libipa", "version": 2, "mbox": "https://patchwork.libcamera.org/series/4260/mbox/" } ], "comments": "https://patchwork.libcamera.org/api/patches/19896/comments/", "check": "pending", "checks": "https://patchwork.libcamera.org/api/patches/19896/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 7FE01C32C9\n\tfor <parsemail@patchwork.libcamera.org>;\n\tWed, 17 Apr 2024 13:16:05 +0000 (UTC)", "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 7BA7163367;\n\tWed, 17 Apr 2024 15:16:04 +0200 (CEST)", "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 7007963354\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 17 Apr 2024 15:15:56 +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 6CE65182C;\n\tWed, 17 Apr 2024 15:15:09 +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=\"U+gjS6+p\"; dkim-atps=neutral", "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1713359709;\n\tbh=8aGp9gL9UtnoQOkZdBhB/faAUlsA0QECsMBeXo3Hk2M=;\n\th=From:To:Cc:Subject:Date:In-Reply-To:References:From;\n\tb=U+gjS6+p4/XK5eCJU+FhJ8viMNb56F6Ze8UUR3y/hWbCkVYrqz1Gyx1tqu7gIN36V\n\tf38fMGc/WeCWS9EmreMoK8dEgwORLVEGVLVnau1n9FZS2DtYVQ50vB0QqW0QgHTGXe\n\tuRNU5H4FaZ5Pfy2GZ9TTY/z1qIJomFeyi6Fz/njk=", "From": "Daniel Scally <dan.scally@ideasonboard.com>", "To": "libcamera-devel@lists.libcamera.org", "Cc": "Daniel Scally <dan.scally@ideasonboard.com>", "Subject": "[PATCH v2 4/8] ipa: libipa: Add MeanLuminanceAgc base class", "Date": "Wed, 17 Apr 2024 14:15:32 +0100", "Message-Id": "<20240417131536.484129-5-dan.scally@ideasonboard.com>", "X-Mailer": "git-send-email 2.34.1", "In-Reply-To": "<20240417131536.484129-1-dan.scally@ideasonboard.com>", "References": "<20240417131536.484129-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 MeanLuminanceAgc 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\nSigned-off-by: Daniel Scally <dan.scally@ideasonboard.com>\n---\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 | 581 ++++++++++++++++++++++++++\n src/ipa/libipa/agc_mean_luminance.h | 91 ++++\n src/ipa/libipa/meson.build | 2 +\n 3 files changed, 674 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..02e223cf\n--- /dev/null\n+++ b/src/ipa/libipa/agc_mean_luminance.cpp\n@@ -0,0 +1,581 @@\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. The algorithm will apply the constraints by calculating the\n+ * Histogram's inter-quantile mean between the given quantiles and ensure that\n+ * the resulting value is the right side of the given target (as defined by the\n+ * boundary and luminance target).\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 an entire image against a target, and\n+ * selecting a value which pushes it as closely as possible towards the 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 expects to\n+ * use the \\ref ExposureModeHelper class to assist in that division, and expects\n+ * the 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 derive an AGC implementation from this class, an IPA\n+ * needs to be able to 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+\n+AgcMeanLuminance::AgcMeanLuminance()\n+\t: frameCount_(0), filteredExposure_(0s), relativeLuminanceTarget_(0)\n+{\n+}\n+\n+/**\n+ * \\brief Parse the relative luminance target from the tuning data\n+ * \\param[in] tuningData The YamlObject holding the algorithm's tuning data\n+ */\n+void AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)\n+{\n+\trelativeLuminanceTarget_ =\n+\t\ttuningData[\"relativeLuminanceTarget\"].get<double>(kDefaultRelativeLuminanceTarget);\n+}\n+\n+/**\n+ * \\brief Parse an AeConstraintMode constraint from tuning data\n+ * \\param[in] modeDict the YamlObject holding the constraint data\n+ * \\param[in] id The constraint ID from AeConstraintModeEnum\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 various Agc algorithms were adhering to\n+\t * anyway before centralisation.\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>();\n+\t\t\thelper->init(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>();\n+\t\thelper->init(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+\tret = parseExposureModes(tuningData);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\treturn 0;\n+}\n+\n+/**\n+ * \\brief configure the ExposureModeHelpers 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::setShutterGainLimits() for each\n+ * ExposureModeHelper that has been created for this class.\n+ */\n+void AgcMeanLuminance::configureExposureModeHelpers(utils::Duration minShutter,\n+\t\t\t\t\t\t utils::Duration maxShutter,\n+\t\t\t\t\t\t double minGain,\n+\t\t\t\t\t\t double maxGain)\n+{\n+\tfor (auto &[id, helper] : exposureModeHelpers_)\n+\t\thelper->setShutterGainLimits(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 implement an overriding\n+ * function that calculates the normalised mean luminance value across the\n+ * entire 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+ *\n+ * To account for non-linearity caused by saturation, the value needs to be\n+ * estimated in an iterative process, as multiplying by a gain will not increase\n+ * the relative luminance by the same factor if some image regions are saturated\n+ *\n+ * \\return The calculated initial gain\n+ */\n+double AgcMeanLuminance::estimateInitialGain()\n+{\n+\tdouble yTarget = relativeLuminanceTarget_;\n+\tdouble yGain = 1.0;\n+\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\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+ *\t the calculated gain\n+ * \\param[in] effectiveExposureValue The EV applied to the frame from which the\n+ *\t 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+\tutils::Duration maxTotalExposure = exposureModeHelper->maxShutter()\n+\t\t\t\t\t * exposureModeHelper->maxGain();\n+\tnewExposureValue = std::min(newExposureValue, maxTotalExposure);\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,\n+ * either in configure() or queueRequest() if the frame number is zero.\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..e48dc498\n--- /dev/null\n+++ b/src/ipa/libipa/agc_mean_luminance.h\n@@ -0,0 +1,91 @@\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 <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() = default;\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 configureExposureModeHelpers(utils::Duration minShutter,\n+\t\t\t\t\t utils::Duration maxShutter,\n+\t\t\t\t\t double minGain,\n+\t\t\t\t\t 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+\tdouble estimateInitialGain();\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+\tstd::tuple<utils::Duration, double, double>\n+\tcalculateNewEv(uint32_t constraintModeIndex, uint32_t exposureModeIndex,\n+\t\t const Histogram &yHist, utils::Duration effectiveExposureValue);\n+\tvoid resetFrameCount() { frameCount_ = 0; }\n+private:\n+\tvirtual double estimateLuminance(const double gain) = 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+\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": [ "v2", "4/8" ] }