Show a patch.

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

{
    "id": 25971,
    "url": "https://patchwork.libcamera.org/api/patches/25971/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/25971/",
    "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": "<20260127120604.6560-2-david.plowman@raspberrypi.com>",
    "date": "2026-01-27T11:59:56",
    "name": "[v5,1/4] ipa: rpi: controller: awb: Separate Bayesian AWB into AwbBayes",
    "commit_ref": null,
    "pull_url": null,
    "state": "accepted",
    "archived": false,
    "hash": "01bd53ff4d2575c0df674d38a56e622aba8af2f0",
    "submitter": {
        "id": 42,
        "url": "https://patchwork.libcamera.org/api/people/42/?format=api",
        "name": "David Plowman",
        "email": "david.plowman@raspberrypi.com"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/25971/mbox/",
    "series": [
        {
            "id": 5741,
            "url": "https://patchwork.libcamera.org/api/series/5741/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=5741",
            "date": "2026-01-27T11:59:55",
            "name": "Raspberry Pi AWB using neural networks",
            "version": 5,
            "mbox": "https://patchwork.libcamera.org/series/5741/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/25971/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/25971/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 E96B5C328D\n\tfor <parsemail@patchwork.libcamera.org>;\n\tTue, 27 Jan 2026 12:06:11 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 8D9BB61FD0;\n\tTue, 27 Jan 2026 13:06:11 +0100 (CET)",
            "from mail-wr1-x42b.google.com (mail-wr1-x42b.google.com\n\t[IPv6:2a00:1450:4864:20::42b])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 1D02561FC4\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 27 Jan 2026 13:06:09 +0100 (CET)",
            "by mail-wr1-x42b.google.com with SMTP id\n\tffacd0b85a97d-42fbc305552so4986188f8f.0\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 27 Jan 2026 04:06:09 -0800 (PST)",
            "from davidp-pi5.pitowers.org\n\t([2a00:1098:3142:1f:88ea:c658:5b20:5e46])\n\tby smtp.gmail.com with ESMTPSA id\n\tffacd0b85a97d-435b1c30293sm38221267f8f.19.2026.01.27.04.06.06\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tTue, 27 Jan 2026 04:06:06 -0800 (PST)"
        ],
        "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=raspberrypi.com header.i=@raspberrypi.com\n\theader.b=\"SgYypk7V\"; dkim-atps=neutral",
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1769515568; x=1770120368;\n\tdarn=lists.libcamera.org; \n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:from:to:cc:subject:date\n\t:message-id:reply-to;\n\tbh=5Lkn3QHgEx5ERDMB+6X3gLv3IBA7gusf6Ya1oVevxuI=;\n\tb=SgYypk7VC8nY7eJv0j1ygm/TfhpkhXwtMnqQSURXIaJnAd3Vzj37Cy2yPj+JH2HhH8\n\tiG6qZ4UgFF0eaHqZoiX0nBxUFSytutyo5tUN/6t28O4g5flYlLJoV4g4+9uZQlPk+aI/\n\te2sCWUtWysv6wmwQnLkhJ32u08KSHfXuWLuTtqrCC7nGMnGWcS1rGvRnjHuKfOsSR1KZ\n\tTJri22VxNCdvo5uqBMpfv0i9dwMckARdRH1L3ktQ0Z++byeyfnBgyb3pXoiASMGyXW40\n\tA8OdCOiJipRVyW5Z+xeST2ewKPjuVhPIsEQWejUZVqHXqhqjM8YFEzbt8YASPxkViZxO\n\tNqsw==",
        "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1769515568; x=1770120368;\n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from\n\t:to:cc:subject:date:message-id:reply-to;\n\tbh=5Lkn3QHgEx5ERDMB+6X3gLv3IBA7gusf6Ya1oVevxuI=;\n\tb=oa96wTek866LUnWjGqiQyUEPPlbpcwlZjM7mEL8JR6cjAZ4Kv/YVCv1UqUL6FDcdBF\n\tH8XXZyEwPF7AZCx+UBhcS18xEeLELBRc6WSCsGpHkZwNKCbQ3lA194sSHr0Rd6mbVLlV\n\tdhxkPgdqgNJTCLhzYVmV8qlVb0sB88168dJYs9kiKwHAptroeLDdIFJIXzpbQT9DO+tb\n\tjJuQtk94zR+gTj5c0rW2dVfOQT3wcPl6HEbrqskSLdkF3ZoMKWprtNWAobgER2oZuKRI\n\tJkTqBgL5O+Nn6EkUdBBaj94FqlQQT4H7kqUyK0wEZRAH6pMeJcpGerCMqWOeOGYGDbtr\n\tiqag==",
        "X-Gm-Message-State": "AOJu0YyiEr8NV4CIb2Zv98aYSA8KxPS9Hj/XmvRELoykZ1tRT+jJTGn7\n\tw8uhIqmWWLybO5LUzcyxHmBXCYHpYLKSleGwHSlYvFEHu99hzbkFS5QWPLAKYD5SKxoYAPCM87g\n\tz3OO+",
        "X-Gm-Gg": "AZuq6aLUftIe50svvAw9mD5be5tES9h6l2Ee6YjuzUYvTtRA0GxMStkAyiNVvWHFskh\n\tZnWYEzWEdoCP13RmZd9JKFSVr5+KmZUyAWcfT04DEift5dLMKoxpQ9XyYWwbwEtT4yzT6mW9tGn\n\toBjciltPC0k15oMMhTNHxC+9/QQ8qlyRjYpIUv4TPUhNBhWKqol6Ih1EH9pL4tW5mh3qZ/7XZWc\n\tK5ZW+5szXg8L9c9YtEpGV6IUjSY8CT7Lc8+B0XAix2LMlMzgWE8d9VDKxArdq9baSw0aTN3kioV\n\tgFRpmyQYp05yRwZZ5ogIogp9fZdAghZuotM5YU+ysvLvwqrg1r0STfMoGzm497LvQNirxv+PVKh\n\trt2AZ0P2Cq3VC7uvpUTTopKERTURKel5sK+uB4XKMoGyobezBb2Lfh8IRx+liXknRDYbuGb9gE/\n\tOlKerrmCQXdhdng+FkznHqI+wThpEf54/dsJvMT/qEcqg+65QMUq5Bq5gpouE/PnvxiOxcwuxzA\n\tMKKNbC9m5EEcvtrwmb4NTHlenRtHQ==",
        "X-Received": "by 2002:a05:6000:2285:b0:431:327:5dd4 with SMTP id\n\tffacd0b85a97d-435dd1bd31dmr2534958f8f.46.1769515567516; \n\tTue, 27 Jan 2026 04:06:07 -0800 (PST)",
        "From": "David Plowman <david.plowman@raspberrypi.com>",
        "To": "libcamera-devel@lists.libcamera.org",
        "Cc": "Peter Bailey <peter.bailey@raspberrypi.com>,\n\tDavid Plowman <david.plowman@raspberrypi.com>,\n\tNaushir Patuck <naush@raspberrypi.com>",
        "Subject": "[PATCH v5 1/4] ipa: rpi: controller: awb: Separate Bayesian AWB into\n\tAwbBayes",
        "Date": "Tue, 27 Jan 2026 11:59:56 +0000",
        "Message-ID": "<20260127120604.6560-2-david.plowman@raspberrypi.com>",
        "X-Mailer": "git-send-email 2.47.3",
        "In-Reply-To": "<20260127120604.6560-1-david.plowman@raspberrypi.com>",
        "References": "<20260127120604.6560-1-david.plowman@raspberrypi.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": "From: Peter Bailey <peter.bailey@raspberrypi.com>\n\nMove parts of the AWB algorithm specific to the Bayesian algorithm into a\nnew class. This will make it easier to add new AWB algorithms in the future.\n\nSigned-off-by: Peter Bailey <peter.bailey@raspberrypi.com>\nReviewed-by: David Plowman <david.plowman@raspberrypi.com>\nReviewed-by: Naushir Patuck <naush@raspberrypi.com>\n---\n src/ipa/rpi/controller/meson.build       |   1 +\n src/ipa/rpi/controller/rpi/awb.cpp       | 409 +++------------------\n src/ipa/rpi/controller/rpi/awb.h         |  99 ++---\n src/ipa/rpi/controller/rpi/awb_bayes.cpp | 444 +++++++++++++++++++++++\n 4 files changed, 533 insertions(+), 420 deletions(-)\n create mode 100644 src/ipa/rpi/controller/rpi/awb_bayes.cpp",
    "diff": "diff --git a/src/ipa/rpi/controller/meson.build b/src/ipa/rpi/controller/meson.build\nindex c13c4853..c8637906 100644\n--- a/src/ipa/rpi/controller/meson.build\n+++ b/src/ipa/rpi/controller/meson.build\n@@ -10,6 +10,7 @@ rpi_ipa_controller_sources = files([\n     'rpi/agc_channel.cpp',\n     'rpi/alsc.cpp',\n     'rpi/awb.cpp',\n+    'rpi/awb_bayes.cpp',\n     'rpi/black_level.cpp',\n     'rpi/cac.cpp',\n     'rpi/ccm.cpp',\ndiff --git a/src/ipa/rpi/controller/rpi/awb.cpp b/src/ipa/rpi/controller/rpi/awb.cpp\nindex 365b595f..de5fa59b 100644\n--- a/src/ipa/rpi/controller/rpi/awb.cpp\n+++ b/src/ipa/rpi/controller/rpi/awb.cpp\n@@ -1,20 +1,14 @@\n /* SPDX-License-Identifier: BSD-2-Clause */\n /*\n- * Copyright (C) 2019, Raspberry Pi Ltd\n+ * Copyright (C) 2025, Raspberry Pi Ltd\n  *\n  * AWB control algorithm\n  */\n-\n-#include <assert.h>\n-#include <cmath>\n-#include <functional>\n-\n-#include <libcamera/base/log.h>\n+#include \"awb.h\"\n \n #include \"../lux_status.h\"\n \n #include \"alsc_status.h\"\n-#include \"awb.h\"\n \n using namespace RPiController;\n using namespace libcamera;\n@@ -23,39 +17,6 @@ LOG_DEFINE_CATEGORY(RPiAwb)\n \n constexpr double kDefaultCT = 4500.0;\n \n-#define NAME \"rpi.awb\"\n-\n-/*\n- * todo - the locking in this algorithm needs some tidying up as has been done\n- * elsewhere (ALSC and AGC).\n- */\n-\n-int AwbMode::read(const libcamera::YamlObject &params)\n-{\n-\tauto value = params[\"lo\"].get<double>();\n-\tif (!value)\n-\t\treturn -EINVAL;\n-\tctLo = *value;\n-\n-\tvalue = params[\"hi\"].get<double>();\n-\tif (!value)\n-\t\treturn -EINVAL;\n-\tctHi = *value;\n-\n-\treturn 0;\n-}\n-\n-int AwbPrior::read(const libcamera::YamlObject &params)\n-{\n-\tauto value = params[\"lux\"].get<double>();\n-\tif (!value)\n-\t\treturn -EINVAL;\n-\tlux = *value;\n-\n-\tprior = params[\"prior\"].get<ipa::Pwl>(ipa::Pwl{});\n-\treturn prior.empty() ? -EINVAL : 0;\n-}\n-\n static int readCtCurve(ipa::Pwl &ctR, ipa::Pwl &ctB, const libcamera::YamlObject &params)\n {\n \tif (params.size() % 3) {\n@@ -92,11 +53,25 @@ static int readCtCurve(ipa::Pwl &ctR, ipa::Pwl &ctB, const libcamera::YamlObject\n \treturn 0;\n }\n \n+int AwbMode::read(const libcamera::YamlObject &params)\n+{\n+\tauto value = params[\"lo\"].get<double>();\n+\tif (!value)\n+\t\treturn -EINVAL;\n+\tctLo = *value;\n+\n+\tvalue = params[\"hi\"].get<double>();\n+\tif (!value)\n+\t\treturn -EINVAL;\n+\tctHi = *value;\n+\n+\treturn 0;\n+}\n+\n int AwbConfig::read(const libcamera::YamlObject &params)\n {\n \tint ret;\n \n-\tbayes = params[\"bayes\"].get<int>(1);\n \tframePeriod = params[\"frame_period\"].get<uint16_t>(10);\n \tstartupFrames = params[\"startup_frames\"].get<uint16_t>(10);\n \tconvergenceFrames = params[\"convergence_frames\"].get<unsigned int>(3);\n@@ -111,23 +86,6 @@ int AwbConfig::read(const libcamera::YamlObject &params)\n \t\tctBInverse = ctB.inverse().first;\n \t}\n \n-\tif (params.contains(\"priors\")) {\n-\t\tfor (const auto &p : params[\"priors\"].asList()) {\n-\t\t\tAwbPrior prior;\n-\t\t\tret = prior.read(p);\n-\t\t\tif (ret)\n-\t\t\t\treturn ret;\n-\t\t\tif (!priors.empty() && prior.lux <= priors.back().lux) {\n-\t\t\t\tLOG(RPiAwb, Error) << \"AwbConfig: Prior must be ordered in increasing lux value\";\n-\t\t\t\treturn -EINVAL;\n-\t\t\t}\n-\t\t\tpriors.push_back(prior);\n-\t\t}\n-\t\tif (priors.empty()) {\n-\t\t\tLOG(RPiAwb, Error) << \"AwbConfig: no AWB priors configured\";\n-\t\t\treturn -EINVAL;\n-\t\t}\n-\t}\n \tif (params.contains(\"modes\")) {\n \t\tfor (const auto &[key, value] : params[\"modes\"].asDict()) {\n \t\t\tret = modes[key].read(value);\n@@ -142,13 +100,10 @@ int AwbConfig::read(const libcamera::YamlObject &params)\n \t\t}\n \t}\n \n-\tminPixels = params[\"min_pixels\"].get<double>(16.0);\n-\tminG = params[\"min_G\"].get<uint16_t>(32);\n-\tminRegions = params[\"min_regions\"].get<uint32_t>(10);\n \tdeltaLimit = params[\"delta_limit\"].get<double>(0.2);\n-\tcoarseStep = params[\"coarse_step\"].get<double>(0.2);\n \ttransversePos = params[\"transverse_pos\"].get<double>(0.01);\n \ttransverseNeg = params[\"transverse_neg\"].get<double>(0.01);\n+\n \tif (transversePos <= 0 || transverseNeg <= 0) {\n \t\tLOG(RPiAwb, Error) << \"AwbConfig: transverse_pos/neg must be > 0\";\n \t\treturn -EINVAL;\n@@ -157,29 +112,21 @@ int AwbConfig::read(const libcamera::YamlObject &params)\n \tsensitivityR = params[\"sensitivity_r\"].get<double>(1.0);\n \tsensitivityB = params[\"sensitivity_b\"].get<double>(1.0);\n \n-\tif (bayes) {\n-\t\tif (ctR.empty() || ctB.empty() || priors.empty() ||\n-\t\t    defaultMode == nullptr) {\n-\t\t\tLOG(RPiAwb, Warning)\n-\t\t\t\t<< \"Bayesian AWB mis-configured - switch to Grey method\";\n-\t\t\tbayes = false;\n-\t\t}\n-\t}\n-\twhitepointR = params[\"whitepoint_r\"].get<double>(0.0);\n-\twhitepointB = params[\"whitepoint_b\"].get<double>(0.0);\n-\tif (bayes == false)\n+\tif (hasCtCurve() && defaultMode != nullptr) {\n+\t\tgreyWorld = false;\n+\t} else {\n+\t\tgreyWorld = true;\n \t\tsensitivityR = sensitivityB = 1.0; /* nor do sensitivities make any sense */\n-\t/*\n-\t * The biasProportion parameter adds a small proportion of the counted\n-\t * pixles to a region biased to the biasCT colour temperature.\n-\t *\n-\t * A typical value for biasProportion would be between 0.05 to 0.1.\n-\t */\n-\tbiasProportion = params[\"bias_proportion\"].get<double>(0.0);\n-\tbiasCT = params[\"bias_ct\"].get<double>(kDefaultCT);\n+\t}\n+\n \treturn 0;\n }\n \n+bool AwbConfig::hasCtCurve() const\n+{\n+\treturn !ctR.empty() && !ctB.empty();\n+}\n+\n Awb::Awb(Controller *controller)\n \t: AwbAlgorithm(controller)\n {\n@@ -199,16 +146,6 @@ Awb::~Awb()\n \tasyncThread_.join();\n }\n \n-char const *Awb::name() const\n-{\n-\treturn NAME;\n-}\n-\n-int Awb::read(const libcamera::YamlObject &params)\n-{\n-\treturn config_.read(params);\n-}\n-\n void Awb::initialise()\n {\n \tframeCount_ = framePhase_ = 0;\n@@ -217,7 +154,7 @@ void Awb::initialise()\n \t * just in case the first few frames don't have anything meaningful in\n \t * them.\n \t */\n-\tif (!config_.ctR.empty() && !config_.ctB.empty()) {\n+\tif (!config_.greyWorld) {\n \t\tsyncResults_.temperatureK = config_.ctR.domain().clamp(4000);\n \t\tsyncResults_.gainR = 1.0 / config_.ctR.eval(syncResults_.temperatureK);\n \t\tsyncResults_.gainG = 1.0;\n@@ -282,7 +219,7 @@ void Awb::setManualGains(double manualR, double manualB)\n \t\tsyncResults_.gainR = prevSyncResults_.gainR = manualR_;\n \t\tsyncResults_.gainG = prevSyncResults_.gainG = 1.0;\n \t\tsyncResults_.gainB = prevSyncResults_.gainB = manualB_;\n-\t\tif (config_.bayes) {\n+\t\tif (!config_.greyWorld) {\n \t\t\t/* Also estimate the best corresponding colour temperature from the curves. */\n \t\t\tdouble ctR = config_.ctRInverse.eval(config_.ctRInverse.domain().clamp(1 / manualR_));\n \t\t\tdouble ctB = config_.ctBInverse.eval(config_.ctBInverse.domain().clamp(1 / manualB_));\n@@ -294,7 +231,7 @@ void Awb::setManualGains(double manualR, double manualB)\n \n void Awb::setColourTemperature(double temperatureK)\n {\n-\tif (!config_.bayes) {\n+\tif (config_.greyWorld) {\n \t\tLOG(RPiAwb, Warning) << \"AWB uncalibrated - cannot set colour temperature\";\n \t\treturn;\n \t}\n@@ -433,10 +370,10 @@ void Awb::asyncFunc()\n \t}\n }\n \n-static void generateStats(std::vector<Awb::RGB> &zones,\n-\t\t\t  StatisticsPtr &stats, double minPixels,\n-\t\t\t  double minG, Metadata &globalMetadata,\n-\t\t\t  double biasProportion, double biasCtR, double biasCtB)\n+void Awb::generateStats(std::vector<Awb::RGB> &zones,\n+\t\t\tStatisticsPtr &stats, double minPixels,\n+\t\t\tdouble minG, Metadata &globalMetadata,\n+\t\t\tdouble biasProportion, double biasCtR, double biasCtB)\n {\n \tstd::scoped_lock<RPiController::Metadata> l(globalMetadata);\n \n@@ -450,9 +387,9 @@ static void generateStats(std::vector<Awb::RGB> &zones,\n \t\t\tzone.R = region.val.rSum / region.counted;\n \t\t\tzone.B = region.val.bSum / region.counted;\n \t\t\t/*\n-\t\t\t * Add some bias samples to allow the search to tend to a\n-\t\t\t * bias CT in failure cases.\n-\t\t\t */\n+\t\t\t* Add some bias samples to allow the search to tend to a\n+\t\t\t* bias CT in failure cases.\n+\t\t\t*/\n \t\t\tconst unsigned int proportion = biasProportion * region.counted;\n \t\t\tzone.R += proportion * biasCtR;\n \t\t\tzone.B += proportion * biasCtB;\n@@ -469,29 +406,7 @@ static void generateStats(std::vector<Awb::RGB> &zones,\n \t}\n }\n \n-void Awb::prepareStats()\n-{\n-\tzones_.clear();\n-\t/*\n-\t * LSC has already been applied to the stats in this pipeline, so stop\n-\t * any LSC compensation.  We also ignore config_.fast in this version.\n-\t */\n-\tconst double biasCtR = config_.bayes ? config_.ctR.eval(config_.biasCT) : 0;\n-\tconst double biasCtB = config_.bayes ? config_.ctB.eval(config_.biasCT) : 0;\n-\tgenerateStats(zones_, statistics_, config_.minPixels,\n-\t\t      config_.minG, getGlobalMetadata(),\n-\t\t      config_.biasProportion, biasCtR, biasCtB);\n-\t/*\n-\t * apply sensitivities, so values appear to come from our \"canonical\"\n-\t * sensor.\n-\t */\n-\tfor (auto &zone : zones_) {\n-\t\tzone.R *= config_.sensitivityR;\n-\t\tzone.B *= config_.sensitivityB;\n-\t}\n-}\n-\n-double Awb::computeDelta2Sum(double gainR, double gainB)\n+double Awb::computeDelta2Sum(double gainR, double gainB, double whitepointR, double whitepointB)\n {\n \t/*\n \t * Compute the sum of the squared colour error (non-greyness) as it\n@@ -499,8 +414,8 @@ double Awb::computeDelta2Sum(double gainR, double gainB)\n \t */\n \tdouble delta2Sum = 0;\n \tfor (auto &z : zones_) {\n-\t\tdouble deltaR = gainR * z.R - 1 - config_.whitepointR;\n-\t\tdouble deltaB = gainB * z.B - 1 - config_.whitepointB;\n+\t\tdouble deltaR = gainR * z.R - 1 - whitepointR;\n+\t\tdouble deltaB = gainB * z.B - 1 - whitepointB;\n \t\tdouble delta2 = deltaR * deltaR + deltaB * deltaB;\n \t\t/* LOG(RPiAwb, Debug) << \"deltaR \" << deltaR << \" deltaB \" << deltaB << \" delta2 \" << delta2; */\n \t\tdelta2 = std::min(delta2, config_.deltaLimit);\n@@ -509,39 +424,14 @@ double Awb::computeDelta2Sum(double gainR, double gainB)\n \treturn delta2Sum;\n }\n \n-ipa::Pwl Awb::interpolatePrior()\n+double Awb::interpolateQuadatric(libcamera::ipa::Pwl::Point const &a,\n+\t\t\t\t libcamera::ipa::Pwl::Point const &b,\n+\t\t\t\t libcamera::ipa::Pwl::Point const &c)\n {\n \t/*\n-\t * Interpolate the prior log likelihood function for our current lux\n-\t * value.\n-\t */\n-\tif (lux_ <= config_.priors.front().lux)\n-\t\treturn config_.priors.front().prior;\n-\telse if (lux_ >= config_.priors.back().lux)\n-\t\treturn config_.priors.back().prior;\n-\telse {\n-\t\tint idx = 0;\n-\t\t/* find which two we lie between */\n-\t\twhile (config_.priors[idx + 1].lux < lux_)\n-\t\t\tidx++;\n-\t\tdouble lux0 = config_.priors[idx].lux,\n-\t\t       lux1 = config_.priors[idx + 1].lux;\n-\t\treturn ipa::Pwl::combine(config_.priors[idx].prior,\n-\t\t\t\t    config_.priors[idx + 1].prior,\n-\t\t\t\t    [&](double /*x*/, double y0, double y1) {\n-\t\t\t\t\t    return y0 + (y1 - y0) *\n-\t\t\t\t\t\t\t(lux_ - lux0) / (lux1 - lux0);\n-\t\t\t\t    });\n-\t}\n-}\n-\n-static double interpolateQuadatric(ipa::Pwl::Point const &a, ipa::Pwl::Point const &b,\n-\t\t\t\t   ipa::Pwl::Point const &c)\n-{\n-\t/*\n-\t * Given 3 points on a curve, find the extremum of the function in that\n-\t * interval by fitting a quadratic.\n-\t */\n+\t* Given 3 points on a curve, find the extremum of the function in that\n+\t* interval by fitting a quadratic.\n+\t*/\n \tconst double eps = 1e-3;\n \tipa::Pwl::Point ca = c - a, ba = b - a;\n \tdouble denominator = 2 * (ba.y() * ca.x() - ca.y() * ba.x());\n@@ -554,180 +444,6 @@ static double interpolateQuadatric(ipa::Pwl::Point const &a, ipa::Pwl::Point con\n \treturn a.y() < c.y() - eps ? a.x() : (c.y() < a.y() - eps ? c.x() : b.x());\n }\n \n-double Awb::coarseSearch(ipa::Pwl const &prior)\n-{\n-\tpoints_.clear(); /* assume doesn't deallocate memory */\n-\tsize_t bestPoint = 0;\n-\tdouble t = mode_->ctLo;\n-\tint spanR = 0, spanB = 0;\n-\t/* Step down the CT curve evaluating log likelihood. */\n-\twhile (true) {\n-\t\tdouble r = config_.ctR.eval(t, &spanR);\n-\t\tdouble b = config_.ctB.eval(t, &spanB);\n-\t\tdouble gainR = 1 / r, gainB = 1 / b;\n-\t\tdouble delta2Sum = computeDelta2Sum(gainR, gainB);\n-\t\tdouble priorLogLikelihood = prior.eval(prior.domain().clamp(t));\n-\t\tdouble finalLogLikelihood = delta2Sum - priorLogLikelihood;\n-\t\tLOG(RPiAwb, Debug)\n-\t\t\t<< \"t: \" << t << \" gain R \" << gainR << \" gain B \"\n-\t\t\t<< gainB << \" delta2_sum \" << delta2Sum\n-\t\t\t<< \" prior \" << priorLogLikelihood << \" final \"\n-\t\t\t<< finalLogLikelihood;\n-\t\tpoints_.push_back(ipa::Pwl::Point({ t, finalLogLikelihood }));\n-\t\tif (points_.back().y() < points_[bestPoint].y())\n-\t\t\tbestPoint = points_.size() - 1;\n-\t\tif (t == mode_->ctHi)\n-\t\t\tbreak;\n-\t\t/* for even steps along the r/b curve scale them by the current t */\n-\t\tt = std::min(t + t / 10 * config_.coarseStep, mode_->ctHi);\n-\t}\n-\tt = points_[bestPoint].x();\n-\tLOG(RPiAwb, Debug) << \"Coarse search found CT \" << t;\n-\t/*\n-\t * We have the best point of the search, but refine it with a quadratic\n-\t * interpolation around its neighbours.\n-\t */\n-\tif (points_.size() > 2) {\n-\t\tunsigned long bp = std::min(bestPoint, points_.size() - 2);\n-\t\tbestPoint = std::max(1UL, bp);\n-\t\tt = interpolateQuadatric(points_[bestPoint - 1],\n-\t\t\t\t\t points_[bestPoint],\n-\t\t\t\t\t points_[bestPoint + 1]);\n-\t\tLOG(RPiAwb, Debug)\n-\t\t\t<< \"After quadratic refinement, coarse search has CT \"\n-\t\t\t<< t;\n-\t}\n-\treturn t;\n-}\n-\n-void Awb::fineSearch(double &t, double &r, double &b, ipa::Pwl const &prior)\n-{\n-\tint spanR = -1, spanB = -1;\n-\tconfig_.ctR.eval(t, &spanR);\n-\tconfig_.ctB.eval(t, &spanB);\n-\tdouble step = t / 10 * config_.coarseStep * 0.1;\n-\tint nsteps = 5;\n-\tdouble rDiff = config_.ctR.eval(t + nsteps * step, &spanR) -\n-\t\t       config_.ctR.eval(t - nsteps * step, &spanR);\n-\tdouble bDiff = config_.ctB.eval(t + nsteps * step, &spanB) -\n-\t\t       config_.ctB.eval(t - nsteps * step, &spanB);\n-\tipa::Pwl::Point transverse({ bDiff, -rDiff });\n-\tif (transverse.length2() < 1e-6)\n-\t\treturn;\n-\t/*\n-\t * unit vector orthogonal to the b vs. r function (pointing outwards\n-\t * with r and b increasing)\n-\t */\n-\ttransverse = transverse / transverse.length();\n-\tdouble bestLogLikelihood = 0, bestT = 0, bestR = 0, bestB = 0;\n-\tdouble transverseRange = config_.transverseNeg + config_.transversePos;\n-\tconst int maxNumDeltas = 12;\n-\t/* a transverse step approximately every 0.01 r/b units */\n-\tint numDeltas = floor(transverseRange * 100 + 0.5) + 1;\n-\tnumDeltas = numDeltas < 3 ? 3 : (numDeltas > maxNumDeltas ? maxNumDeltas : numDeltas);\n-\t/*\n-\t * Step down CT curve. March a bit further if the transverse range is\n-\t * large.\n-\t */\n-\tnsteps += numDeltas;\n-\tfor (int i = -nsteps; i <= nsteps; i++) {\n-\t\tdouble tTest = t + i * step;\n-\t\tdouble priorLogLikelihood =\n-\t\t\tprior.eval(prior.domain().clamp(tTest));\n-\t\tdouble rCurve = config_.ctR.eval(tTest, &spanR);\n-\t\tdouble bCurve = config_.ctB.eval(tTest, &spanB);\n-\t\t/* x will be distance off the curve, y the log likelihood there */\n-\t\tipa::Pwl::Point points[maxNumDeltas];\n-\t\tint bestPoint = 0;\n-\t\t/* Take some measurements transversely *off* the CT curve. */\n-\t\tfor (int j = 0; j < numDeltas; j++) {\n-\t\t\tpoints[j][0] = -config_.transverseNeg +\n-\t\t\t\t       (transverseRange * j) / (numDeltas - 1);\n-\t\t\tipa::Pwl::Point rbTest = ipa::Pwl::Point({ rCurve, bCurve }) +\n-\t\t\t\t\t\t transverse * points[j].x();\n-\t\t\tdouble rTest = rbTest.x(), bTest = rbTest.y();\n-\t\t\tdouble gainR = 1 / rTest, gainB = 1 / bTest;\n-\t\t\tdouble delta2Sum = computeDelta2Sum(gainR, gainB);\n-\t\t\tpoints[j][1] = delta2Sum - priorLogLikelihood;\n-\t\t\tLOG(RPiAwb, Debug)\n-\t\t\t\t<< \"At t \" << tTest << \" r \" << rTest << \" b \"\n-\t\t\t\t<< bTest << \": \" << points[j].y();\n-\t\t\tif (points[j].y() < points[bestPoint].y())\n-\t\t\t\tbestPoint = j;\n-\t\t}\n-\t\t/*\n-\t\t * We have NUM_DELTAS points transversely across the CT curve,\n-\t\t * now let's do a quadratic interpolation for the best result.\n-\t\t */\n-\t\tbestPoint = std::max(1, std::min(bestPoint, numDeltas - 2));\n-\t\tipa::Pwl::Point rbTest = ipa::Pwl::Point({ rCurve, bCurve }) +\n-\t\t\t\t\t transverse * interpolateQuadatric(points[bestPoint - 1],\n-\t\t\t\t\t\t\t\t\t   points[bestPoint],\n-\t\t\t\t\t\t\t\t\t   points[bestPoint + 1]);\n-\t\tdouble rTest = rbTest.x(), bTest = rbTest.y();\n-\t\tdouble gainR = 1 / rTest, gainB = 1 / bTest;\n-\t\tdouble delta2Sum = computeDelta2Sum(gainR, gainB);\n-\t\tdouble finalLogLikelihood = delta2Sum - priorLogLikelihood;\n-\t\tLOG(RPiAwb, Debug)\n-\t\t\t<< \"Finally \"\n-\t\t\t<< tTest << \" r \" << rTest << \" b \" << bTest << \": \"\n-\t\t\t<< finalLogLikelihood\n-\t\t\t<< (finalLogLikelihood < bestLogLikelihood ? \" BEST\" : \"\");\n-\t\tif (bestT == 0 || finalLogLikelihood < bestLogLikelihood)\n-\t\t\tbestLogLikelihood = finalLogLikelihood,\n-\t\t\tbestT = tTest, bestR = rTest, bestB = bTest;\n-\t}\n-\tt = bestT, r = bestR, b = bestB;\n-\tLOG(RPiAwb, Debug)\n-\t\t<< \"Fine search found t \" << t << \" r \" << r << \" b \" << b;\n-}\n-\n-void Awb::awbBayes()\n-{\n-\t/*\n-\t * May as well divide out G to save computeDelta2Sum from doing it over\n-\t * and over.\n-\t */\n-\tfor (auto &z : zones_)\n-\t\tz.R = z.R / (z.G + 1), z.B = z.B / (z.G + 1);\n-\t/*\n-\t * Get the current prior, and scale according to how many zones are\n-\t * valid... not entirely sure about this.\n-\t */\n-\tipa::Pwl prior = interpolatePrior();\n-\tprior *= zones_.size() / (double)(statistics_->awbRegions.numRegions());\n-\tprior.map([](double x, double y) {\n-\t\tLOG(RPiAwb, Debug) << \"(\" << x << \",\" << y << \")\";\n-\t});\n-\tdouble t = coarseSearch(prior);\n-\tdouble r = config_.ctR.eval(t);\n-\tdouble b = config_.ctB.eval(t);\n-\tLOG(RPiAwb, Debug)\n-\t\t<< \"After coarse search: r \" << r << \" b \" << b << \" (gains r \"\n-\t\t<< 1 / r << \" b \" << 1 / b << \")\";\n-\t/*\n-\t * Not entirely sure how to handle the fine search yet. Mostly the\n-\t * estimated CT is already good enough, but the fine search allows us to\n-\t * wander transverely off the CT curve. Under some illuminants, where\n-\t * there may be more or less green light, this may prove beneficial,\n-\t * though I probably need more real datasets before deciding exactly how\n-\t * this should be controlled and tuned.\n-\t */\n-\tfineSearch(t, r, b, prior);\n-\tLOG(RPiAwb, Debug)\n-\t\t<< \"After fine search: r \" << r << \" b \" << b << \" (gains r \"\n-\t\t<< 1 / r << \" b \" << 1 / b << \")\";\n-\t/*\n-\t * Write results out for the main thread to pick up. Remember to adjust\n-\t * the gains from the ones that the \"canonical sensor\" would require to\n-\t * the ones needed by *this* sensor.\n-\t */\n-\tasyncResults_.temperatureK = t;\n-\tasyncResults_.gainR = 1.0 / r * config_.sensitivityR;\n-\tasyncResults_.gainG = 1.0;\n-\tasyncResults_.gainB = 1.0 / b * config_.sensitivityB;\n-}\n-\n void Awb::awbGrey()\n {\n \tLOG(RPiAwb, Debug) << \"Grey world AWB\";\n@@ -765,32 +481,3 @@ void Awb::awbGrey()\n \tasyncResults_.gainG = 1.0;\n \tasyncResults_.gainB = gainB;\n }\n-\n-void Awb::doAwb()\n-{\n-\tprepareStats();\n-\tLOG(RPiAwb, Debug) << \"Valid zones: \" << zones_.size();\n-\tif (zones_.size() > config_.minRegions) {\n-\t\tif (config_.bayes)\n-\t\t\tawbBayes();\n-\t\telse\n-\t\t\tawbGrey();\n-\t\tLOG(RPiAwb, Debug)\n-\t\t\t<< \"CT found is \"\n-\t\t\t<< asyncResults_.temperatureK\n-\t\t\t<< \" with gains r \" << asyncResults_.gainR\n-\t\t\t<< \" and b \" << asyncResults_.gainB;\n-\t}\n-\t/*\n-\t * we're done with these; we may as well relinquish our hold on the\n-\t * pointer.\n-\t */\n-\tstatistics_.reset();\n-}\n-\n-/* Register algorithm with the system. */\n-static Algorithm *create(Controller *controller)\n-{\n-\treturn (Algorithm *)new Awb(controller);\n-}\n-static RegisterAlgorithm reg(NAME, &create);\ndiff --git a/src/ipa/rpi/controller/rpi/awb.h b/src/ipa/rpi/controller/rpi/awb.h\nindex 2fb91254..8b2d8d1d 100644\n--- a/src/ipa/rpi/controller/rpi/awb.h\n+++ b/src/ipa/rpi/controller/rpi/awb.h\n@@ -1,42 +1,33 @@\n /* SPDX-License-Identifier: BSD-2-Clause */\n /*\n- * Copyright (C) 2019, Raspberry Pi Ltd\n+ * Copyright (C) 2025, Raspberry Pi Ltd\n  *\n  * AWB control algorithm\n  */\n #pragma once\n \n-#include <mutex>\n #include <condition_variable>\n+#include <mutex>\n #include <thread>\n \n-#include <libcamera/geometry.h>\n-\n #include \"../awb_algorithm.h\"\n #include \"../awb_status.h\"\n-#include \"../statistics.h\"\n-\n #include \"libipa/pwl.h\"\n \n namespace RPiController {\n \n-/* Control algorithm to perform AWB calculations. */\n-\n struct AwbMode {\n \tint read(const libcamera::YamlObject &params);\n \tdouble ctLo; /* low CT value for search */\n \tdouble ctHi; /* high CT value for search */\n };\n \n-struct AwbPrior {\n-\tint read(const libcamera::YamlObject &params);\n-\tdouble lux; /* lux level */\n-\tlibcamera::ipa::Pwl prior; /* maps CT to prior log likelihood for this lux level */\n-};\n-\n struct AwbConfig {\n-\tAwbConfig() : defaultMode(nullptr) {}\n+\tAwbConfig()\n+\t\t: defaultMode(nullptr) {}\n \tint read(const libcamera::YamlObject &params);\n+\tbool hasCtCurve() const;\n+\n \t/* Only repeat the AWB calculation every \"this many\" frames */\n \tuint16_t framePeriod;\n \t/* number of initial frames for which speed taken as 1.0 (maximum) */\n@@ -47,27 +38,13 @@ struct AwbConfig {\n \tlibcamera::ipa::Pwl ctB; /* function maps CT to b (= B/G) */\n \tlibcamera::ipa::Pwl ctRInverse; /* inverse of ctR */\n \tlibcamera::ipa::Pwl ctBInverse; /* inverse of ctB */\n-\t/* table of illuminant priors at different lux levels */\n-\tstd::vector<AwbPrior> priors;\n+\n \t/* AWB \"modes\" (determines the search range) */\n \tstd::map<std::string, AwbMode> modes;\n \tAwbMode *defaultMode; /* mode used if no mode selected */\n-\t/*\n-\t * minimum proportion of pixels counted within AWB region for it to be\n-\t * \"useful\"\n-\t */\n-\tdouble minPixels;\n-\t/* minimum G value of those pixels, to be regarded a \"useful\" */\n-\tuint16_t minG;\n-\t/*\n-\t * number of AWB regions that must be \"useful\" in order to do the AWB\n-\t * calculation\n-\t */\n-\tuint32_t minRegions;\n+\n \t/* clamp on colour error term (so as not to penalise non-grey excessively) */\n \tdouble deltaLimit;\n-\t/* step size control in coarse search */\n-\tdouble coarseStep;\n \t/* how far to wander off CT curve towards \"more purple\" */\n \tdouble transversePos;\n \t/* how far to wander off CT curve towards \"more green\" */\n@@ -82,14 +59,8 @@ struct AwbConfig {\n \t * sensor's B/G)\n \t */\n \tdouble sensitivityB;\n-\t/* The whitepoint (which we normally \"aim\" for) can be moved. */\n-\tdouble whitepointR;\n-\tdouble whitepointB;\n-\tbool bayes; /* use Bayesian algorithm */\n-\t/* proportion of counted samples to add for the search bias */\n-\tdouble biasProportion;\n-\t/* CT target for the search bias */\n-\tdouble biasCT;\n+\n+\tbool greyWorld; /* don't use the ct curve when in grey world mode */\n };\n \n class Awb : public AwbAlgorithm\n@@ -97,9 +68,7 @@ class Awb : public AwbAlgorithm\n public:\n \tAwb(Controller *controller = NULL);\n \t~Awb();\n-\tchar const *name() const override;\n-\tvoid initialise() override;\n-\tint read(const libcamera::YamlObject &params) override;\n+\tvirtual void initialise() override;\n \tunsigned int getConvergenceFrames() const override;\n \tvoid initialValues(double &gainR, double &gainB) override;\n \tvoid setMode(std::string const &name) override;\n@@ -110,6 +79,11 @@ public:\n \tvoid switchMode(CameraMode const &cameraMode, Metadata *metadata) override;\n \tvoid prepare(Metadata *imageMetadata) override;\n \tvoid process(StatisticsPtr &stats, Metadata *imageMetadata) override;\n+\n+\tstatic double interpolateQuadatric(libcamera::ipa::Pwl::Point const &a,\n+\t\t\t\t\t   libcamera::ipa::Pwl::Point const &b,\n+\t\t\t\t\t   libcamera::ipa::Pwl::Point const &c);\n+\n \tstruct RGB {\n \t\tRGB(double r = 0, double g = 0, double b = 0)\n \t\t\t: R(r), G(g), B(b)\n@@ -123,10 +97,30 @@ public:\n \t\t}\n \t};\n \n-private:\n-\tbool isAutoEnabled() const;\n+protected:\n \t/* configuration is read-only, and available to both threads */\n \tAwbConfig config_;\n+\t/*\n+\t * The following are for the asynchronous thread to use, though the main\n+\t * thread can set/reset them if the async thread is known to be idle:\n+\t */\n+\tstd::vector<RGB> zones_;\n+\tStatisticsPtr statistics_;\n+\tdouble lux_;\n+\tAwbMode *mode_;\n+\tAwbStatus asyncResults_;\n+\n+\tvirtual void doAwb() = 0;\n+\tvirtual void prepareStats() = 0;\n+\tdouble computeDelta2Sum(double gainR, double gainB, double whitepointR, double whitepointB);\n+\tvoid awbGrey();\n+\tstatic void generateStats(std::vector<Awb::RGB> &zones,\n+\t\t\t\t  StatisticsPtr &stats, double minPixels,\n+\t\t\t\t  double minG, Metadata &globalMetadata,\n+\t\t\t\t  double biasProportion, double biasCtR, double biasCtB);\n+\n+private:\n+\tbool isAutoEnabled() const;\n \tstd::thread asyncThread_;\n \tvoid asyncFunc(); /* asynchronous thread function */\n \tstd::mutex mutex_;\n@@ -152,6 +146,7 @@ private:\n \tAwbStatus syncResults_;\n \tAwbStatus prevSyncResults_;\n \tstd::string modeName_;\n+\n \t/*\n \t * The following are for the asynchronous thread to use, though the main\n \t * thread can set/reset them if the async thread is known to be idle:\n@@ -159,20 +154,6 @@ private:\n \tvoid restartAsync(StatisticsPtr &stats, double lux);\n \t/* copy out the results from the async thread so that it can be restarted */\n \tvoid fetchAsyncResults();\n-\tStatisticsPtr statistics_;\n-\tAwbMode *mode_;\n-\tdouble lux_;\n-\tAwbStatus asyncResults_;\n-\tvoid doAwb();\n-\tvoid awbBayes();\n-\tvoid awbGrey();\n-\tvoid prepareStats();\n-\tdouble computeDelta2Sum(double gainR, double gainB);\n-\tlibcamera::ipa::Pwl interpolatePrior();\n-\tdouble coarseSearch(libcamera::ipa::Pwl const &prior);\n-\tvoid fineSearch(double &t, double &r, double &b, libcamera::ipa::Pwl const &prior);\n-\tstd::vector<RGB> zones_;\n-\tstd::vector<libcamera::ipa::Pwl::Point> points_;\n \t/* manual r setting */\n \tdouble manualR_;\n \t/* manual b setting */\n@@ -196,4 +177,4 @@ static inline Awb::RGB operator*(Awb::RGB const &rgb, double d)\n \treturn d * rgb;\n }\n \n-} /* namespace RPiController */\n+} // namespace RPiController\ndiff --git a/src/ipa/rpi/controller/rpi/awb_bayes.cpp b/src/ipa/rpi/controller/rpi/awb_bayes.cpp\nnew file mode 100644\nindex 00000000..09233cec\n--- /dev/null\n+++ b/src/ipa/rpi/controller/rpi/awb_bayes.cpp\n@@ -0,0 +1,444 @@\n+/* SPDX-License-Identifier: BSD-2-Clause */\n+/*\n+ * Copyright (C) 2019, Raspberry Pi Ltd\n+ *\n+ * AWB control algorithm\n+ */\n+\n+#include <assert.h>\n+#include <cmath>\n+#include <condition_variable>\n+#include <functional>\n+#include <mutex>\n+#include <thread>\n+\n+#include <libcamera/base/log.h>\n+\n+#include <libcamera/geometry.h>\n+\n+#include \"../awb_algorithm.h\"\n+#include \"../awb_status.h\"\n+#include \"../lux_status.h\"\n+#include \"../statistics.h\"\n+#include \"libipa/pwl.h\"\n+\n+#include \"alsc_status.h\"\n+#include \"awb.h\"\n+\n+using namespace libcamera;\n+\n+LOG_DECLARE_CATEGORY(RPiAwb)\n+\n+constexpr double kDefaultCT = 4500.0;\n+\n+#define NAME \"rpi.awb\"\n+\n+/*\n+ * todo - the locking in this algorithm needs some tidying up as has been done\n+ * elsewhere (ALSC and AGC).\n+ */\n+\n+namespace RPiController {\n+\n+struct AwbPrior {\n+\tint read(const libcamera::YamlObject &params);\n+\tdouble lux; /* lux level */\n+\tlibcamera::ipa::Pwl prior; /* maps CT to prior log likelihood for this lux level */\n+};\n+\n+struct AwbBayesConfig {\n+\tAwbBayesConfig() {}\n+\tint read(const libcamera::YamlObject &params, AwbConfig &config);\n+\t/* table of illuminant priors at different lux levels */\n+\tstd::vector<AwbPrior> priors;\n+\t/*\n+\t * minimum proportion of pixels counted within AWB region for it to be\n+\t * \"useful\"\n+\t */\n+\tdouble minPixels;\n+\t/* minimum G value of those pixels, to be regarded a \"useful\" */\n+\tuint16_t minG;\n+\t/*\n+\t * number of AWB regions that must be \"useful\" in order to do the AWB\n+\t * calculation\n+\t */\n+\tuint32_t minRegions;\n+\t/* step size control in coarse search */\n+\tdouble coarseStep;\n+\t/* The whitepoint (which we normally \"aim\" for) can be moved. */\n+\tdouble whitepointR;\n+\tdouble whitepointB;\n+\tbool bayes; /* use Bayesian algorithm */\n+\t/* proportion of counted samples to add for the search bias */\n+\tdouble biasProportion;\n+\t/* CT target for the search bias */\n+\tdouble biasCT;\n+};\n+\n+class AwbBayes : public Awb\n+{\n+public:\n+\tAwbBayes(Controller *controller = NULL);\n+\t~AwbBayes();\n+\tchar const *name() const override;\n+\tint read(const libcamera::YamlObject &params) override;\n+\n+protected:\n+\tvoid prepareStats() override;\n+\tvoid doAwb() override;\n+\n+private:\n+\tAwbBayesConfig bayesConfig_;\n+\tvoid awbBayes();\n+\tlibcamera::ipa::Pwl interpolatePrior();\n+\tdouble coarseSearch(libcamera::ipa::Pwl const &prior);\n+\tvoid fineSearch(double &t, double &r, double &b, libcamera::ipa::Pwl const &prior);\n+\tstd::vector<libcamera::ipa::Pwl::Point> points_;\n+};\n+\n+int AwbPrior::read(const libcamera::YamlObject &params)\n+{\n+\tauto value = params[\"lux\"].get<double>();\n+\tif (!value)\n+\t\treturn -EINVAL;\n+\tlux = *value;\n+\n+\tprior = params[\"prior\"].get<ipa::Pwl>(ipa::Pwl{});\n+\treturn prior.empty() ? -EINVAL : 0;\n+}\n+\n+int AwbBayesConfig::read(const libcamera::YamlObject &params, AwbConfig &config)\n+{\n+\tint ret;\n+\n+\tbayes = params[\"bayes\"].get<int>(1);\n+\n+\tif (params.contains(\"priors\")) {\n+\t\tfor (const auto &p : params[\"priors\"].asList()) {\n+\t\t\tAwbPrior prior;\n+\t\t\tret = prior.read(p);\n+\t\t\tif (ret)\n+\t\t\t\treturn ret;\n+\t\t\tif (!priors.empty() && prior.lux <= priors.back().lux) {\n+\t\t\t\tLOG(RPiAwb, Error) << \"AwbConfig: Prior must be ordered in increasing lux value\";\n+\t\t\t\treturn -EINVAL;\n+\t\t\t}\n+\t\t\tpriors.push_back(prior);\n+\t\t}\n+\t\tif (priors.empty()) {\n+\t\t\tLOG(RPiAwb, Error) << \"AwbConfig: no AWB priors configured\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\t}\n+\n+\tminPixels = params[\"min_pixels\"].get<double>(16.0);\n+\tminG = params[\"min_G\"].get<uint16_t>(32);\n+\tminRegions = params[\"min_regions\"].get<uint32_t>(10);\n+\tcoarseStep = params[\"coarse_step\"].get<double>(0.2);\n+\n+\tif (bayes) {\n+\t\tif (!config.hasCtCurve() || priors.empty() ||\n+\t\t    config.defaultMode == nullptr) {\n+\t\t\tLOG(RPiAwb, Warning)\n+\t\t\t\t<< \"Bayesian AWB mis-configured - switch to Grey method\";\n+\t\t\tbayes = false;\n+\t\t}\n+\t}\n+\twhitepointR = params[\"whitepoint_r\"].get<double>(0.0);\n+\twhitepointB = params[\"whitepoint_b\"].get<double>(0.0);\n+\tif (bayes == false) {\n+\t\tconfig.sensitivityR = config.sensitivityB = 1.0; /* nor do sensitivities make any sense */\n+\t\tconfig.greyWorld = true; /* prevent the ct curve being used in manual mode */\n+\t}\n+\t/*\n+\t * The biasProportion parameter adds a small proportion of the counted\n+\t * pixles to a region biased to the biasCT colour temperature.\n+\t *\n+\t * A typical value for biasProportion would be between 0.05 to 0.1.\n+\t */\n+\tbiasProportion = params[\"bias_proportion\"].get<double>(0.0);\n+\tbiasCT = params[\"bias_ct\"].get<double>(kDefaultCT);\n+\treturn 0;\n+}\n+\n+AwbBayes::AwbBayes(Controller *controller)\n+\t: Awb(controller)\n+{\n+}\n+\n+AwbBayes::~AwbBayes()\n+{\n+}\n+\n+char const *AwbBayes::name() const\n+{\n+\treturn NAME;\n+}\n+\n+int AwbBayes::read(const libcamera::YamlObject &params)\n+{\n+\tint ret;\n+\n+\tret = config_.read(params);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tret = bayesConfig_.read(params, config_);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\treturn 0;\n+}\n+\n+void AwbBayes::prepareStats()\n+{\n+\tzones_.clear();\n+\t/*\n+\t * LSC has already been applied to the stats in this pipeline, so stop\n+\t * any LSC compensation.  We also ignore config_.fast in this version.\n+\t */\n+\tconst double biasCtR = bayesConfig_.bayes ? config_.ctR.eval(bayesConfig_.biasCT) : 0;\n+\tconst double biasCtB = bayesConfig_.bayes ? config_.ctB.eval(bayesConfig_.biasCT) : 0;\n+\tgenerateStats(zones_, statistics_, bayesConfig_.minPixels,\n+\t\t      bayesConfig_.minG, getGlobalMetadata(),\n+\t\t      bayesConfig_.biasProportion, biasCtR, biasCtB);\n+\t/*\n+\t * apply sensitivities, so values appear to come from our \"canonical\"\n+\t * sensor.\n+\t */\n+\tfor (auto &zone : zones_) {\n+\t\tzone.R *= config_.sensitivityR;\n+\t\tzone.B *= config_.sensitivityB;\n+\t}\n+}\n+\n+ipa::Pwl AwbBayes::interpolatePrior()\n+{\n+\t/*\n+\t * Interpolate the prior log likelihood function for our current lux\n+\t * value.\n+\t */\n+\tif (lux_ <= bayesConfig_.priors.front().lux)\n+\t\treturn bayesConfig_.priors.front().prior;\n+\telse if (lux_ >= bayesConfig_.priors.back().lux)\n+\t\treturn bayesConfig_.priors.back().prior;\n+\telse {\n+\t\tint idx = 0;\n+\t\t/* find which two we lie between */\n+\t\twhile (bayesConfig_.priors[idx + 1].lux < lux_)\n+\t\t\tidx++;\n+\t\tdouble lux0 = bayesConfig_.priors[idx].lux,\n+\t\t       lux1 = bayesConfig_.priors[idx + 1].lux;\n+\t\treturn ipa::Pwl::combine(bayesConfig_.priors[idx].prior,\n+\t\t\t\t\t bayesConfig_.priors[idx + 1].prior,\n+\t\t\t\t\t [&](double /*x*/, double y0, double y1) {\n+\t\t\t\t\t\t return y0 + (y1 - y0) *\n+\t\t\t\t\t\t\t\t     (lux_ - lux0) / (lux1 - lux0);\n+\t\t\t\t\t });\n+\t}\n+}\n+\n+double AwbBayes::coarseSearch(ipa::Pwl const &prior)\n+{\n+\tpoints_.clear(); /* assume doesn't deallocate memory */\n+\tsize_t bestPoint = 0;\n+\tdouble t = mode_->ctLo;\n+\tint spanR = 0, spanB = 0;\n+\t/* Step down the CT curve evaluating log likelihood. */\n+\twhile (true) {\n+\t\tdouble r = config_.ctR.eval(t, &spanR);\n+\t\tdouble b = config_.ctB.eval(t, &spanB);\n+\t\tdouble gainR = 1 / r, gainB = 1 / b;\n+\t\tdouble delta2Sum = computeDelta2Sum(gainR, gainB, bayesConfig_.whitepointR, bayesConfig_.whitepointB);\n+\t\tdouble priorLogLikelihood = prior.eval(prior.domain().clamp(t));\n+\t\tdouble finalLogLikelihood = delta2Sum - priorLogLikelihood;\n+\t\tLOG(RPiAwb, Debug)\n+\t\t\t<< \"t: \" << t << \" gain R \" << gainR << \" gain B \"\n+\t\t\t<< gainB << \" delta2_sum \" << delta2Sum\n+\t\t\t<< \" prior \" << priorLogLikelihood << \" final \"\n+\t\t\t<< finalLogLikelihood;\n+\t\tpoints_.push_back(ipa::Pwl::Point({ t, finalLogLikelihood }));\n+\t\tif (points_.back().y() < points_[bestPoint].y())\n+\t\t\tbestPoint = points_.size() - 1;\n+\t\tif (t == mode_->ctHi)\n+\t\t\tbreak;\n+\t\t/* for even steps along the r/b curve scale them by the current t */\n+\t\tt = std::min(t + t / 10 * bayesConfig_.coarseStep, mode_->ctHi);\n+\t}\n+\tt = points_[bestPoint].x();\n+\tLOG(RPiAwb, Debug) << \"Coarse search found CT \" << t;\n+\t/*\n+\t * We have the best point of the search, but refine it with a quadratic\n+\t * interpolation around its neighbours.\n+\t */\n+\tif (points_.size() > 2) {\n+\t\tunsigned long bp = std::min(bestPoint, points_.size() - 2);\n+\t\tbestPoint = std::max(1UL, bp);\n+\t\tt = interpolateQuadatric(points_[bestPoint - 1],\n+\t\t\t\t\t points_[bestPoint],\n+\t\t\t\t\t points_[bestPoint + 1]);\n+\t\tLOG(RPiAwb, Debug)\n+\t\t\t<< \"After quadratic refinement, coarse search has CT \"\n+\t\t\t<< t;\n+\t}\n+\treturn t;\n+}\n+\n+void AwbBayes::fineSearch(double &t, double &r, double &b, ipa::Pwl const &prior)\n+{\n+\tint spanR = -1, spanB = -1;\n+\tconfig_.ctR.eval(t, &spanR);\n+\tconfig_.ctB.eval(t, &spanB);\n+\tdouble step = t / 10 * bayesConfig_.coarseStep * 0.1;\n+\tint nsteps = 5;\n+\tdouble rDiff = config_.ctR.eval(t + nsteps * step, &spanR) -\n+\t\t       config_.ctR.eval(t - nsteps * step, &spanR);\n+\tdouble bDiff = config_.ctB.eval(t + nsteps * step, &spanB) -\n+\t\t       config_.ctB.eval(t - nsteps * step, &spanB);\n+\tipa::Pwl::Point transverse({ bDiff, -rDiff });\n+\tif (transverse.length2() < 1e-6)\n+\t\treturn;\n+\t/*\n+\t * unit vector orthogonal to the b vs. r function (pointing outwards\n+\t * with r and b increasing)\n+\t */\n+\ttransverse = transverse / transverse.length();\n+\tdouble bestLogLikelihood = 0, bestT = 0, bestR = 0, bestB = 0;\n+\tdouble transverseRange = config_.transverseNeg + config_.transversePos;\n+\tconst int maxNumDeltas = 12;\n+\t/* a transverse step approximately every 0.01 r/b units */\n+\tint numDeltas = floor(transverseRange * 100 + 0.5) + 1;\n+\tnumDeltas = numDeltas < 3 ? 3 : (numDeltas > maxNumDeltas ? maxNumDeltas : numDeltas);\n+\t/*\n+\t * Step down CT curve. March a bit further if the transverse range is\n+\t * large.\n+\t */\n+\tnsteps += numDeltas;\n+\tfor (int i = -nsteps; i <= nsteps; i++) {\n+\t\tdouble tTest = t + i * step;\n+\t\tdouble priorLogLikelihood =\n+\t\t\tprior.eval(prior.domain().clamp(tTest));\n+\t\tdouble rCurve = config_.ctR.eval(tTest, &spanR);\n+\t\tdouble bCurve = config_.ctB.eval(tTest, &spanB);\n+\t\t/* x will be distance off the curve, y the log likelihood there */\n+\t\tipa::Pwl::Point points[maxNumDeltas];\n+\t\tint bestPoint = 0;\n+\t\t/* Take some measurements transversely *off* the CT curve. */\n+\t\tfor (int j = 0; j < numDeltas; j++) {\n+\t\t\tpoints[j][0] = -config_.transverseNeg +\n+\t\t\t\t       (transverseRange * j) / (numDeltas - 1);\n+\t\t\tipa::Pwl::Point rbTest = ipa::Pwl::Point({ rCurve, bCurve }) +\n+\t\t\t\t\t\t transverse * points[j].x();\n+\t\t\tdouble rTest = rbTest.x(), bTest = rbTest.y();\n+\t\t\tdouble gainR = 1 / rTest, gainB = 1 / bTest;\n+\t\t\tdouble delta2Sum = computeDelta2Sum(gainR, gainB, bayesConfig_.whitepointR, bayesConfig_.whitepointB);\n+\t\t\tpoints[j][1] = delta2Sum - priorLogLikelihood;\n+\t\t\tLOG(RPiAwb, Debug)\n+\t\t\t\t<< \"At t \" << tTest << \" r \" << rTest << \" b \"\n+\t\t\t\t<< bTest << \": \" << points[j].y();\n+\t\t\tif (points[j].y() < points[bestPoint].y())\n+\t\t\t\tbestPoint = j;\n+\t\t}\n+\t\t/*\n+\t\t * We have NUM_DELTAS points transversely across the CT curve,\n+\t\t * now let's do a quadratic interpolation for the best result.\n+\t\t */\n+\t\tbestPoint = std::max(1, std::min(bestPoint, numDeltas - 2));\n+\t\tipa::Pwl::Point rbTest = ipa::Pwl::Point({ rCurve, bCurve }) +\n+\t\t\t\t\t transverse * interpolateQuadatric(points[bestPoint - 1],\n+\t\t\t\t\t\t\t\t\t   points[bestPoint],\n+\t\t\t\t\t\t\t\t\t   points[bestPoint + 1]);\n+\t\tdouble rTest = rbTest.x(), bTest = rbTest.y();\n+\t\tdouble gainR = 1 / rTest, gainB = 1 / bTest;\n+\t\tdouble delta2Sum = computeDelta2Sum(gainR, gainB, bayesConfig_.whitepointR, bayesConfig_.whitepointB);\n+\t\tdouble finalLogLikelihood = delta2Sum - priorLogLikelihood;\n+\t\tLOG(RPiAwb, Debug)\n+\t\t\t<< \"Finally \"\n+\t\t\t<< tTest << \" r \" << rTest << \" b \" << bTest << \": \"\n+\t\t\t<< finalLogLikelihood\n+\t\t\t<< (finalLogLikelihood < bestLogLikelihood ? \" BEST\" : \"\");\n+\t\tif (bestT == 0 || finalLogLikelihood < bestLogLikelihood)\n+\t\t\tbestLogLikelihood = finalLogLikelihood,\n+\t\t\tbestT = tTest, bestR = rTest, bestB = bTest;\n+\t}\n+\tt = bestT, r = bestR, b = bestB;\n+\tLOG(RPiAwb, Debug)\n+\t\t<< \"Fine search found t \" << t << \" r \" << r << \" b \" << b;\n+}\n+\n+void AwbBayes::awbBayes()\n+{\n+\t/*\n+\t * May as well divide out G to save computeDelta2Sum from doing it over\n+\t * and over.\n+\t */\n+\tfor (auto &z : zones_)\n+\t\tz.R = z.R / (z.G + 1), z.B = z.B / (z.G + 1);\n+\t/*\n+\t * Get the current prior, and scale according to how many zones are\n+\t * valid... not entirely sure about this.\n+\t */\n+\tipa::Pwl prior = interpolatePrior();\n+\tprior *= zones_.size() / (double)(statistics_->awbRegions.numRegions());\n+\tprior.map([](double x, double y) {\n+\t\tLOG(RPiAwb, Debug) << \"(\" << x << \",\" << y << \")\";\n+\t});\n+\tdouble t = coarseSearch(prior);\n+\tdouble r = config_.ctR.eval(t);\n+\tdouble b = config_.ctB.eval(t);\n+\tLOG(RPiAwb, Debug)\n+\t\t<< \"After coarse search: r \" << r << \" b \" << b << \" (gains r \"\n+\t\t<< 1 / r << \" b \" << 1 / b << \")\";\n+\t/*\n+\t * Not entirely sure how to handle the fine search yet. Mostly the\n+\t * estimated CT is already good enough, but the fine search allows us to\n+\t * wander transverely off the CT curve. Under some illuminants, where\n+\t * there may be more or less green light, this may prove beneficial,\n+\t * though I probably need more real datasets before deciding exactly how\n+\t * this should be controlled and tuned.\n+\t */\n+\tfineSearch(t, r, b, prior);\n+\tLOG(RPiAwb, Debug)\n+\t\t<< \"After fine search: r \" << r << \" b \" << b << \" (gains r \"\n+\t\t<< 1 / r << \" b \" << 1 / b << \")\";\n+\t/*\n+\t * Write results out for the main thread to pick up. Remember to adjust\n+\t * the gains from the ones that the \"canonical sensor\" would require to\n+\t * the ones needed by *this* sensor.\n+\t */\n+\tasyncResults_.temperatureK = t;\n+\tasyncResults_.gainR = 1.0 / r * config_.sensitivityR;\n+\tasyncResults_.gainG = 1.0;\n+\tasyncResults_.gainB = 1.0 / b * config_.sensitivityB;\n+}\n+\n+void AwbBayes::doAwb()\n+{\n+\tprepareStats();\n+\tLOG(RPiAwb, Debug) << \"Valid zones: \" << zones_.size();\n+\tif (zones_.size() > bayesConfig_.minRegions) {\n+\t\tif (bayesConfig_.bayes)\n+\t\t\tawbBayes();\n+\t\telse\n+\t\t\tawbGrey();\n+\t\tLOG(RPiAwb, Debug)\n+\t\t\t<< \"CT found is \"\n+\t\t\t<< asyncResults_.temperatureK\n+\t\t\t<< \" with gains r \" << asyncResults_.gainR\n+\t\t\t<< \" and b \" << asyncResults_.gainB;\n+\t}\n+\t/*\n+\t * we're done with these; we may as well relinquish our hold on the\n+\t * pointer.\n+\t */\n+\tstatistics_.reset();\n+}\n+\n+/* Register algorithm with the system. */\n+static Algorithm *create(Controller *controller)\n+{\n+\treturn (Algorithm *)new AwbBayes(controller);\n+}\n+static RegisterAlgorithm reg(NAME, &create);\n+\n+} /* namespace RPiController */\n",
    "prefixes": [
        "v5",
        "1/4"
    ]
}