Patch Detail
Show a patch.
GET /api/patches/19791/?format=api
{ "id": 19791, "url": "https://patchwork.libcamera.org/api/patches/19791/?format=api", "web_url": "https://patchwork.libcamera.org/patch/19791/", "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": "<20240322131451.3092931-5-dan.scally@ideasonboard.com>", "date": "2024-03-22T13:14:45", "name": "[04/10] ipa: libipa: Add MeanLuminanceAgc base class", "commit_ref": null, "pull_url": null, "state": "superseded", "archived": false, "hash": "534ff07dd9608337ad8343673e117f0734fa3414", "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/19791/mbox/", "series": [ { "id": 4236, "url": "https://patchwork.libcamera.org/api/series/4236/?format=api", "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=4236", "date": "2024-03-22T13:14:41", "name": "Centralise Agc into libipa", "version": 1, "mbox": "https://patchwork.libcamera.org/series/4236/mbox/" } ], "comments": "https://patchwork.libcamera.org/api/patches/19791/comments/", "check": "pending", "checks": "https://patchwork.libcamera.org/api/patches/19791/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 BC212C3274\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 22 Mar 2024 13:15:20 +0000 (UTC)", "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id C023263311;\n\tFri, 22 Mar 2024 14:15:17 +0100 (CET)", "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 4592F63055\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 22 Mar 2024 14:15:09 +0100 (CET)", "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 2CA8C842;\n\tFri, 22 Mar 2024 14:14:40 +0100 (CET)" ], "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"l7mKzC2V\"; dkim-atps=neutral", "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1711113280;\n\tbh=S4cGW8CYQljeYizRjaOnjh9CA1KlaF98ppm+qKwygLQ=;\n\th=From:To:Cc:Subject:Date:In-Reply-To:References:From;\n\tb=l7mKzC2VjeTUC6l/MdlIHZDeHiGDp2kg+FGWv+jm8LeVN0VYmZbArW3YWUFqONQQp\n\twdXxt1QY5nEidkBMC+ZHdVU+pF+MmxrEDzQHvgl0oOiTjcUh2reea3RcEsdYSvRhEe\n\thjd7xZO2dgic6Nl2j7hp0Mqgp0FHFGru8gFSiIBI=", "From": "Daniel Scally <dan.scally@ideasonboard.com>", "To": "libcamera-devel@lists.libcamera.org", "Cc": "Daniel Scally <dan.scally@ideasonboard.com>", "Subject": "[PATCH 04/10] ipa: libipa: Add MeanLuminanceAgc base class", "Date": "Fri, 22 Mar 2024 13:14:45 +0000", "Message-Id": "<20240322131451.3092931-5-dan.scally@ideasonboard.com>", "X-Mailer": "git-send-email 2.34.1", "In-Reply-To": "<20240322131451.3092931-1-dan.scally@ideasonboard.com>", "References": "<20240322131451.3092931-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---\n src/ipa/libipa/agc.cpp | 526 +++++++++++++++++++++++++++++++++++++\n src/ipa/libipa/agc.h | 82 ++++++\n src/ipa/libipa/meson.build | 2 +\n 3 files changed, 610 insertions(+)\n create mode 100644 src/ipa/libipa/agc.cpp\n create mode 100644 src/ipa/libipa/agc.h", "diff": "diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp\nnew file mode 100644\nindex 00000000..af57a571\n--- /dev/null\n+++ b/src/ipa/libipa/agc.cpp\n@@ -0,0 +1,526 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024 Ideas on Board Oy\n+ *\n+ * agc.cpp - Base class for libipa-compliant AGC algorithms\n+ */\n+\n+#include \"agc.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.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(Agc)\n+\n+namespace ipa {\n+\n+/*\n+ * Number of frames to wait before calculating stats on minimum exposure\n+ * \\todo should this be a tunable value?\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 MeanLuminanceAgc::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 MeanLuminanceAgc::AgcConstraint::Bound\n+ * \\brief Specify whether the constraint defines a lower or upper bound\n+ * \\var MeanLuminanceAgc::AgcConstraint::LOWER\n+ * \\brief The constraint defines a lower bound\n+ * \\var MeanLuminanceAgc::AgcConstraint::UPPER\n+ * \\brief The constraint defines an upper bound\n+ */\n+\n+/**\n+ * \\var MeanLuminanceAgc::AgcConstraint::bound\n+ * \\brief The type of constraint bound\n+ */\n+\n+/**\n+ * \\var MeanLuminanceAgc::AgcConstraint::qLo\n+ * \\brief The lower quantile to use for the constraint\n+ */\n+\n+/**\n+ * \\var MeanLuminanceAgc::AgcConstraint::qHi\n+ * \\brief The upper quantile to use for the constraint\n+ */\n+\n+/**\n+ * \\var MeanLuminanceAgc::AgcConstraint::yTarget\n+ * \\brief The luminance target for the constraint\n+ */\n+\n+/**\n+ * \\class MeanLuminanceAgc\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. The gain from the first stage is then clamped to the gain from\n+ * this stage.\n+ *\n+ * The final gain is used to adjust the effective exposure value of the image,\n+ * and that new exposure value divided into shutter time, analogue gain and\n+ * digital gain according to the selected AeExposureMode.\n+ */\n+\n+MeanLuminanceAgc::MeanLuminanceAgc()\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 MeanLuminanceAgc::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 MeanLuminanceAgc::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(Agc, 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+/**\n+ * \\brief Parse tuning data file to populate AeConstraintMode control\n+ * \\param[in] tuningData the YamlObject representing the tuning data for Agc\n+ *\n+ * The Agc algorithm's tuning data should contain a dictionary called\n+ * AeConstraintMode containing per-mode setting dictionaries with the key being\n+ * a value from \\ref controls::AeConstraintModeNameValueMap. Each mode dict may\n+ * contain either a \"lower\" or \"upper\" key, or both, in this format:\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+ * The parsed dictionaries are used to populate an array of available values for\n+ * the AeConstraintMode control and stored for later use in the algorithm.\n+ *\n+ * \\return -EINVAL Where a defined constraint mode is invalid\n+ * \\return 0 on success\n+ */\n+int MeanLuminanceAgc::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(Agc, 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(Agc, 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+/**\n+ * \\brief Parse tuning data file to populate AeExposureMode control\n+ * \\param[in] tuningData the YamlObject representing the tuning data for Agc\n+ *\n+ * The Agc algorithm's tuning data should contain a dictionary called\n+ * AeExposureMode containing per-mode setting dictionaries with the key being\n+ * a 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: [ 1.0, 2.0, 4.0, 6.0, 6.0 ]\n+ * ExposureShort:\n+ * shutter: [ 100, 10000, 30000, 60000, 120000 ]\n+ * gain: [ 1.0, 2.0, 4.0, 6.0, 6.0 ]\n+ *\n+ * \\endcode\n+ *\n+ * The parsed dictionaries are used to populate an array of available values for\n+ * the AeExposureMode control and to create ExposureModeHelpers\n+ *\n+ * \\return -EINVAL Where a defined constraint mode is invalid\n+ * \\return 0 on success\n+ */\n+int MeanLuminanceAgc::parseExposureModes(const YamlObject &tuningData)\n+{\n+\tstd::vector<ControlValue> availableExposureModes;\n+\tint ret;\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(Agc, 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(Agc, 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\tstd::vector<utils::Duration> shutterDurations;\n+\t\t\tstd::transform(shutters.begin(), shutters.end(),\n+\t\t\t\tstd::back_inserter(shutterDurations),\n+\t\t\t\t[](uint32_t time) { return std::chrono::microseconds(time); });\n+\n+\t\t\tstd::shared_ptr<ExposureModeHelper> helper =\n+\t\t\t\tstd::make_shared<ExposureModeHelper>();\n+\t\t\tif ((ret = helper->init(shutterDurations, gains) < 0)) {\n+\t\t\t\tLOG(Agc, Error)\n+\t\t\t\t\t<< \"Failed to parse exposure mode '\" << modeName << \"'\";\n+\t\t\t\treturn ret;\n+\t\t\t}\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 empty shutter time and gain arrays, which\n+\t * will then internally simply drive the shutter as high as possible\n+\t * before touching gain\n+\t */\n+\tif (availableExposureModes.empty()) {\n+\t\tint32_t exposureModeId = AeExposureModeNameValueMap.at(\"ExposureNormal\");\n+\t\tstd::vector<utils::Duration> shutterDurations = {};\n+\t\tstd::vector<double> gains = {};\n+\n+\t\tstd::shared_ptr<ExposureModeHelper> helper =\n+\t\t\tstd::make_shared<ExposureModeHelper>();\n+\t\tif ((ret = helper->init(shutterDurations, gains) < 0)) {\n+\t\t\tLOG(Agc, Error)\n+\t\t\t\t<< \"Failed to create default ExposureModeHelper\";\n+\t\t\treturn ret;\n+\t\t}\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+ * \\fn MeanLuminanceAgc::constraintModes()\n+ * \\brief Get the constraint modes that have been parsed from tuning data\n+ */\n+\n+/**\n+ * \\fn MeanLuminanceAgc::exposureModeHelpers()\n+ * \\brief Get the ExposureModeHelpers that have been parsed from tuning data\n+ */\n+\n+/**\n+ * \\fn MeanLuminanceAgc::controls()\n+ * \\brief Get the controls that have been generated after parsing tuning data\n+ */\n+\n+/**\n+ * \\fn MeanLuminanceAgc::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 is a pure virtual function because estimation of luminance is a\n+ * hardware-specific operation, which depends wholly on the format of the stats\n+ * that are delivered to libcamera from the ISP. Derived classes must implement\n+ * an overriding function that calculates the normalised mean luminance value\n+ * across the 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 MeanLuminanceAgc::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(Agc, 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 MeanLuminanceAgc::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 MeanLuminanceAgc::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+MeanLuminanceAgc::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+}; /* namespace ipa */\n+\n+}; /* namespace libcamera */\ndiff --git a/src/ipa/libipa/agc.h b/src/ipa/libipa/agc.h\nnew file mode 100644\nindex 00000000..902a359a\n--- /dev/null\n+++ b/src/ipa/libipa/agc.h\n@@ -0,0 +1,82 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024 Ideas on Board Oy\n+ *\n+ agc.h - Base class for libipa-compliant 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 MeanLuminanceAgc\n+{\n+public:\n+\tMeanLuminanceAgc();\n+\tvirtual ~MeanLuminanceAgc() = 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+\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+\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+\tvirtual double estimateLuminance(const double gain) = 0;\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+private:\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..31cc8d70 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.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.cpp',\n 'algorithm.cpp',\n 'camera_sensor_helper.cpp',\n 'exposure_mode_helper.cpp',\n", "prefixes": [ "04/10" ] }