Show a patch.

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

{
    "id": 22496,
    "url": "https://patchwork.libcamera.org/api/patches/22496/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/22496/",
    "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": "<20250109115412.356768-10-stefan.klug@ideasonboard.com>",
    "date": "2025-01-09T11:54:00",
    "name": "[v1,09/11] libipa: Add bayesian AWB algorithm",
    "commit_ref": null,
    "pull_url": null,
    "state": "superseded",
    "archived": false,
    "hash": "defa1efb6c83f394941ea9fca8f4f01f3f8732c0",
    "submitter": {
        "id": 184,
        "url": "https://patchwork.libcamera.org/api/people/184/?format=api",
        "name": "Stefan Klug",
        "email": "stefan.klug@ideasonboard.com"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/22496/mbox/",
    "series": [
        {
            "id": 4938,
            "url": "https://patchwork.libcamera.org/api/series/4938/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=4938",
            "date": "2025-01-09T11:53:51",
            "name": "Add Bayesian AWB algorithm to libipa and rkisp1",
            "version": 1,
            "mbox": "https://patchwork.libcamera.org/series/4938/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/22496/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/22496/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 9ADEAC32EA\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu,  9 Jan 2025 11:55:35 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 6E47E68543;\n\tThu,  9 Jan 2025 12:55:34 +0100 (CET)",
            "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 9F6FE68543\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu,  9 Jan 2025 12:55:29 +0100 (CET)",
            "from ideasonboard.com (unknown\n\t[IPv6:2a00:6020:448c:6c00:93b9:eca8:897d:eae6])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 0F1036F3;\n\tThu,  9 Jan 2025 12:54:36 +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=\"rbpSD3AH\"; dkim-atps=neutral",
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1736423676;\n\tbh=RXjL+83YbrnyIi8mdVx0Qlsf/GxksgPYpwzss/VtojQ=;\n\th=From:To:Cc:Subject:Date:In-Reply-To:References:From;\n\tb=rbpSD3AHlnvRjknDk8F5iT/pKBMmgn1vKOFzYTeJqGghZlcC94gLRvorgt4tpOqsC\n\tInQD5IV86PQJBJHajKIbynTR1u6csiasYOdCsgfpF0kW32Qwe1bIHlZ3Czo12hHipc\n\t4Je9VtULqwJSkESG67d4iPZelfSNmzKyH5008LQY=",
        "From": "Stefan Klug <stefan.klug@ideasonboard.com>",
        "To": "libcamera-devel@lists.libcamera.org",
        "Cc": "Stefan Klug <stefan.klug@ideasonboard.com>",
        "Subject": "[PATCH v1 09/11] libipa: Add bayesian AWB algorithm",
        "Date": "Thu,  9 Jan 2025 12:54:00 +0100",
        "Message-ID": "<20250109115412.356768-10-stefan.klug@ideasonboard.com>",
        "X-Mailer": "git-send-email 2.43.0",
        "In-Reply-To": "<20250109115412.356768-1-stefan.klug@ideasonboard.com>",
        "References": "<20250109115412.356768-1-stefan.klug@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 bayesian AWB algorithm is an AWB algorithm that takes prior\nprobabilities for a given light source dependent on the current lux\nlevel into account.\n\nThe biggest improvement compared to the grey world model comes from the\nsearch of the ideal white point on the CT curve. The algorithm walks the\nCT curve to minimize the colour error for a given statistics. After the\nminimium is found it additionally tries to search the area around that\nspot and also off the curve. So even without defined prior probabilities\nthis algorithm provides much better results than the grey world\nalgorithm.\n\nThe logic for this code was taken from the RaspberryPi implementation.\nThe logic was only minimally adjusted for usage with the rkisp1 and a\nfew things were left out (see doxygen doc for the AwbBayes class). The\ncode is refactored to better fit the libcamera code style and to make\nuse of the syntactic sugar provided by the Interpolator and Vector\nclasses.\n\nSigned-off-by: Stefan Klug <stefan.klug@ideasonboard.com>\n---\n src/ipa/libipa/awb_bayes.cpp | 457 +++++++++++++++++++++++++++++++++++\n src/ipa/libipa/awb_bayes.h   |  67 +++++\n src/ipa/libipa/meson.build   |   2 +\n 3 files changed, 526 insertions(+)\n create mode 100644 src/ipa/libipa/awb_bayes.cpp\n create mode 100644 src/ipa/libipa/awb_bayes.h",
    "diff": "diff --git a/src/ipa/libipa/awb_bayes.cpp b/src/ipa/libipa/awb_bayes.cpp\nnew file mode 100644\nindex 000000000000..1e69ecd3e3f3\n--- /dev/null\n+++ b/src/ipa/libipa/awb_bayes.cpp\n@@ -0,0 +1,457 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2019, Raspberry Pi Ltd\n+ * Copyright (C) 2024 Ideas on Board Oy\n+ *\n+ * Implementation of a bayesian AWB algorithm\n+ */\n+\n+#include \"awb_bayes.h\"\n+\n+#include <cmath>\n+\n+#include <libcamera/base/log.h>\n+#include <libcamera/control_ids.h>\n+\n+#include \"colours.h\"\n+\n+/**\n+ * \\file awb_bayes.h\n+ * \\brief Implementation of bayesian auto white balance algorithm\n+ *\n+ * This implementation is based on the initial implementation done by\n+ * RaspberryPi.\n+ * \\todo: Documentation\n+ *\n+ * \\todo As the statistics module of the rkisp1 provides less data than the one\n+ * from the RaspberryPi (vc4). The RaspberryPi statistics measure a grid of\n+ * zones while the rkisp1 ony measures a single area. Therefore this algorithm\n+ * doesn't contain all the features implemented by RaspberryPi.\n+ * The following parts are not implemented:\n+ *\n+ * - min_pixels: minimum proportion of pixels counted within AWB region for it\n+ *   to be \"useful\"\n+ * - min_g: minimum G value of those pixels, to be regarded a \"useful\"\n+ * - min_regions: number of AWB regions that must be \"useful\" in order to do the\n+ *   AWB calculation\n+ * - deltaLimit: clamp on colour error term (so as not to penalize non-grey\n+ *   excessively)\n+ * - bias_proportion: The biasProportion parameter adds a small proportion of\n+ *   the counted pixels to a region biased to the biasCT colour temperature.\n+ *   A typical value for biasProportion would be between 0.05 to 0.1.\n+ * - bias_ct: CT target for the search bias\n+ * - sensitivityR: red sensitivity ratio (set to canonical sensor's R/G divided\n+ *   by this sensor's R/G)\n+ * - sensitivityB: blue sensitivity ratio (set to canonical sensor's B/G divided\n+ *   by this sensor's B/G)\n+ */\n+\n+namespace libcamera {\n+\n+LOG_DECLARE_CATEGORY(Awb)\n+\n+namespace ipa {\n+\n+/**\n+ * \\brief Step size control for CT search\n+ */\n+constexpr double kSearchStep = 0.2;\n+\n+/**\n+ * \\copydoc Interpolator::interpolate()\n+ */\n+template<>\n+void Interpolator<Pwl>::interpolate(const Pwl &a, const Pwl &b, Pwl &dest, double lambda)\n+{\n+\tdest = Pwl::combine(a, b,\n+\t\t\t    [=](double /*x*/, double y0, double y1) -> double {\n+\t\t\t\t    return y0 * (1.0 - lambda) + y1 * lambda;\n+\t\t\t    });\n+}\n+\n+/**\n+ * \\class AwbBayes\n+ * \\brief Implementation of a bayesian auto white balance algorithm\n+ *\n+ * In a bayesian AWB algorithm the auto white balance estimation is improved by\n+ * taking the likelihood of a given lightsource based on the estimated lux level\n+ * into account. E.g. If it is very bright we can assume that we are outside and\n+ * that colour temperatures around 6500 are preferred.\n+ *\n+ * The second part of this algorithm is the search for the most likely colour\n+ * temperature. It is implemented in AwbBayes::coarseSearch() and in\n+ * AwbBayes::fineSearch(). The search works very well without prior likelihoods\n+ * and therefore the algorithm itself provides very good results even without\n+ * prior likelihoods.\n+ */\n+\n+/**\n+ * \\var AwbBayes::transversePos_\n+ * \\brief How far to wander off CT curve towards \"more purple\"\n+ */\n+\n+/**\n+ * \\var AwbBayes::transverseNeg_\n+ * \\brief How far to wander off CT curve towards \"more green\"\n+ */\n+\n+/**\n+ * \\var AwbBayes::currentMode_\n+ * \\brief The currently selected mode\n+ */\n+\n+int AwbBayes::init(const YamlObject &tuningData)\n+{\n+\tint ret = colourGainCurve_.readYaml(tuningData[\"colourGains\"], \"ct\", \"gains\");\n+\tif (ret) {\n+\t\tLOG(Awb, Error)\n+\t\t\t<< \"Failed to parse 'colourGains' \"\n+\t\t\t<< \"parameter from tuning file\";\n+\t\treturn ret;\n+\t}\n+\n+\tctR_.clear();\n+\tctB_.clear();\n+\tfor (const auto &[ct, g] : colourGainCurve_.data()) {\n+\t\tctR_.append(ct, 1.0 / g[0]);\n+\t\tctB_.append(ct, 1.0 / g[1]);\n+\t}\n+\n+\t/* We will want the inverse functions of these too. */\n+\tctRInverse_ = ctR_.inverse().first;\n+\tctBInverse_ = ctB_.inverse().first;\n+\n+\tret = readPriors(tuningData);\n+\tif (ret) {\n+\t\tLOG(Awb, Error) << \"Failed to read priors\";\n+\t\treturn ret;\n+\t}\n+\n+\tret = parseModeConfigs(tuningData);\n+\tif (ret) {\n+\t\tLOG(Awb, Error)\n+\t\t\t<< \"Failed to parse mode parameter from tuning file\";\n+\t\treturn ret;\n+\t}\n+\tcurrentMode_ = &modes_[controls::AwbAuto];\n+\n+\ttransversePos_ = tuningData[\"transversePos\"].get<double>(0.01);\n+\ttransverseNeg_ = tuningData[\"transverseNeg\"].get<double>(0.01);\n+\tif (transversePos_ <= 0 || transverseNeg_ <= 0) {\n+\t\tLOG(Awb, Error) << \"AwbConfig: transversePos/Neg must be > 0\";\n+\t\treturn -EINVAL;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+int AwbBayes::readPriors(const YamlObject &tuningData)\n+{\n+\tconst auto &priorsList = tuningData[\"priors\"];\n+\tstd::map<uint32_t, Pwl> priors;\n+\n+\tif (!priorsList) {\n+\t\tLOG(Awb, Error) << \"No priors specified\";\n+\t\treturn -EINVAL;\n+\t}\n+\n+\tfor (const auto &p : priorsList.asList()) {\n+\t\tif (!p.contains(\"lux\")) {\n+\t\t\tLOG(Awb, Error) << \"Missing lux value\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\tuint32_t lux = p[\"lux\"].get<uint32_t>(0);\n+\t\tif (priors.count(lux)) {\n+\t\t\tLOG(Awb, Error) << \"Duplicate prior for lux value \" << lux;\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\tstd::vector<uint32_t> temperatures =\n+\t\t\tp[\"ct\"].getList<uint32_t>().value_or(std::vector<uint32_t>{});\n+\t\tstd::vector<double> probabilites =\n+\t\t\tp[\"probability\"].getList<double>().value_or(std::vector<double>{});\n+\n+\t\tif (temperatures.size() != probabilites.size()) {\n+\t\t\tLOG(Awb, Error)\n+\t\t\t\t<< \"Ct and probability array sizes are unequal\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\tif (temperatures.empty()) {\n+\t\t\tLOG(Awb, Error)\n+\t\t\t\t<< \"Ct and probability arrays are empty\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\tstd::map<int, double> ctToProbability;\n+\t\tfor (unsigned int i = 0; i < temperatures.size(); i++) {\n+\t\t\tint t = temperatures[i];\n+\t\t\tif (ctToProbability.count(t)) {\n+\t\t\t\tLOG(Awb, Error) << \"Duplicate ct value\";\n+\t\t\t\treturn -EINVAL;\n+\t\t\t}\n+\n+\t\t\tctToProbability[t] = probabilites[i];\n+\t\t}\n+\n+\t\tauto &pwl = priors[lux];\n+\t\tfor (const auto &[ct, prob] : ctToProbability) {\n+\t\t\tpwl.append(ct, prob);\n+\t\t}\n+\t}\n+\n+\tif (priors.empty()) {\n+\t\tLOG(Awb, Error) << \"No priors specified\";\n+\t\t;\n+\t\treturn -EINVAL;\n+\t}\n+\n+\tpriors_.setData(std::move(priors));\n+\n+\treturn 0;\n+}\n+\n+void AwbBayes::handleControls(const ControlList &controls)\n+{\n+\tauto mode = controls.get(controls::AwbMode);\n+\tif (mode) {\n+\t\tauto it = modes_.find(static_cast<controls::AwbModeEnum>(*mode));\n+\t\tif (it != modes_.end())\n+\t\t\tcurrentMode_ = &it->second;\n+\t\telse\n+\t\t\tLOG(Awb, Error) << \"Unknown AWB mode\";\n+\t}\n+}\n+\n+RGB<double> AwbBayes::gainsFromColourTemperature(double colourTemperature)\n+{\n+\t/*\n+\t * \\todo: In the RaspberryPi code, the ct curve was interpolated in\n+\t * the white point space (1/x) not in gains space. This feels counter\n+\t * intuitive, as the gains are in linear space. But I can't prove it.\n+\t */\n+\tconst auto &gains = colourGainCurve_.getInterpolated(colourTemperature);\n+\treturn { { gains[0], 1.0, gains[1] } };\n+}\n+\n+AwbResult AwbBayes::calculateAwb(const AwbStats &stats, int lux)\n+{\n+\tipa::Pwl prior;\n+\tif (lux > 0) {\n+\t\tprior = priors_.getInterpolated(lux);\n+\t\tprior.map([](double x, double y) {\n+\t\t\tLOG(Awb, Debug) << \"(\" << x << \",\" << y << \")\";\n+\t\t});\n+\t} else {\n+\t\tprior.append(0, 1.0);\n+\t}\n+\n+\tdouble t = coarseSearch(prior, stats);\n+\tdouble r = ctR_.eval(t);\n+\tdouble b = ctB_.eval(t);\n+\tLOG(Awb, Debug)\n+\t\t<< \"After coarse search: r \" << r << \" b \" << b << \" (gains r \"\n+\t\t<< 1 / r << \" b \" << 1 / b << \")\";\n+\n+\t/*\n+\t * Original comment from RaspberryPi:\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 transversely 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, stats);\n+\tLOG(Awb, Debug)\n+\t\t<< \"After fine search: r \" << r << \" b \" << b << \" (gains r \"\n+\t\t<< 1 / r << \" b \" << 1 / b << \")\";\n+\n+\treturn { { { 1.0 / r, 1.0, 1.0 / b } }, t };\n+}\n+\n+double AwbBayes::coarseSearch(const ipa::Pwl &prior, const AwbStats &stats) const\n+{\n+\tstd::vector<Pwl::Point> points;\n+\tsize_t bestPoint = 0;\n+\tdouble t = currentMode_->ctLo;\n+\tint spanR = -1;\n+\tint spanB = -1;\n+\n+\t/* Step down the CT curve evaluating log likelihood. */\n+\twhile (true) {\n+\t\tdouble r = ctR_.eval(t, &spanR);\n+\t\tdouble b = ctB_.eval(t, &spanB);\n+\t\tRGB<double> gains({ 1 / r, 1.0, 1 / b });\n+\t\tdouble delta2Sum = stats.computeColourError(gains);\n+\t\tdouble priorLogLikelihood = prior.eval(prior.domain().clamp(t));\n+\t\tdouble finalLogLikelihood = delta2Sum - priorLogLikelihood;\n+\n+\t\tLOG(Awb, Debug) << \"Coarse search t: \" << t\n+\t\t\t\t<< \" gains: \" << gains\n+\t\t\t\t<< \" error: \" << delta2Sum\n+\t\t\t\t<< \" prior: \" << priorLogLikelihood\n+\t\t\t\t<< \" likelihood: \" << finalLogLikelihood;\n+\n+\t\tpoints.push_back({ { t, finalLogLikelihood } });\n+\t\tif (points.back().y() < points[bestPoint].y())\n+\t\t\tbestPoint = points.size() - 1;\n+\n+\t\tif (t == currentMode_->ctHi)\n+\t\t\tbreak;\n+\n+\t\t/*\n+\t\t * Ensure even steps along the r/b curve by scaling them by the\n+\t\t * current t.\n+\t\t */\n+\t\tt = std::min(t + t / 10 * kSearchStep, currentMode_->ctHi);\n+\t}\n+\n+\tt = points[bestPoint].x();\n+\tLOG(Awb, Debug) << \"Coarse search found CT \" << t;\n+\n+\t/*\n+\t * We have the best point of the search, but refine it with a quadratic\n+\t * interpolation around its neighbors.\n+\t */\n+\tif (points.size() > 2) {\n+\t\tbestPoint = std::clamp(bestPoint, 1ul, points.size() - 2);\n+\t\tt = interpolateQuadratic(points[bestPoint - 1],\n+\t\t\t\t\t points[bestPoint],\n+\t\t\t\t\t points[bestPoint + 1]);\n+\t\tLOG(Awb, Debug)\n+\t\t\t<< \"After quadratic refinement, coarse search has CT \"\n+\t\t\t<< t;\n+\t}\n+\n+\treturn t;\n+}\n+\n+void AwbBayes::fineSearch(double &t, double &r, double &b, ipa::Pwl const &prior, const AwbStats &stats) const\n+{\n+\tint spanR = -1;\n+\tint spanB = -1;\n+\tdouble step = t / 10 * kSearchStep * 0.1;\n+\tint nsteps = 5;\n+\tctR_.eval(t, &spanR);\n+\tctB_.eval(t, &spanB);\n+\tdouble rDiff = ctR_.eval(t + nsteps * step, &spanR) -\n+\t\t       ctR_.eval(t - nsteps * step, &spanR);\n+\tdouble bDiff = ctB_.eval(t + nsteps * step, &spanB) -\n+\t\t       ctB_.eval(t - nsteps * step, &spanB);\n+\tPwl::Point transverse({ bDiff, -rDiff });\n+\tif (transverse.length2() < 1e-6)\n+\t\treturn;\n+\t/*\n+\t * transverse is a unit vector orthogonal to the b vs. r function\n+\t * (pointing outwards with r and b increasing)\n+\t */\n+\ttransverse = transverse / transverse.length();\n+\tdouble bestLogLikelihood = 0;\n+\tdouble bestT = 0;\n+\tPwl::Point bestRB;\n+\tdouble transverseRange = transverseNeg_ + transversePos_;\n+\tconst int maxNumDeltas = 12;\n+\n+\t/* a transverse step approximately every 0.01 r/b units */\n+\tint numDeltas = floor(transverseRange * 100 + 0.5) + 1;\n+\tnumDeltas = std::clamp(numDeltas, 3, maxNumDeltas);\n+\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\tPwl::Point rbStart{ { ctR_.eval(tTest, &spanR),\n+\t\t\t\t      ctB_.eval(tTest, &spanB) } };\n+\t\tPwl::Point samples[maxNumDeltas];\n+\t\tint bestPoint = 0;\n+\n+\t\t/*\n+\t\t * Sample numDeltas points transversely *off* the CT curve\n+\t\t * in the range [-transverseNeg, transversePos].\n+\t\t * The x values of a sample contains the distance and the y\n+\t\t * value contains the corresponding log likelihood.\n+\t\t */\n+\t\tdouble transverseStep = transverseRange / (numDeltas - 1);\n+\t\tfor (int j = 0; j < numDeltas; j++) {\n+\t\t\tauto &p = samples[j];\n+\t\t\tp.x() = -transverseNeg_ + transverseStep * j;\n+\t\t\tPwl::Point rbTest = rbStart + transverse * p.x();\n+\t\t\tRGB<double> gains({ 1 / rbTest[0], 1.0, 1 / rbTest[1] });\n+\t\t\tdouble delta2Sum = stats.computeColourError(gains);\n+\t\t\tp.y() = delta2Sum - priorLogLikelihood;\n+\n+\t\t\tif (p.y() < samples[bestPoint].y())\n+\t\t\t\tbestPoint = j;\n+\t\t}\n+\n+\t\t/*\n+\t\t * We have all samples 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::clamp(bestPoint, 1, numDeltas - 2);\n+\t\tdouble bestOffset = interpolateQuadratic(samples[bestPoint - 1],\n+\t\t\t\t\t\t\t samples[bestPoint],\n+\t\t\t\t\t\t\t samples[bestPoint + 1]);\n+\t\tPwl::Point rbTest = rbStart + transverse * bestOffset;\n+\t\tRGB<double> gains({ 1 / rbTest[0], 1.0, 1 / rbTest[1] });\n+\t\tdouble delta2Sum = stats.computeColourError(gains);\n+\t\tdouble finalLogLikelihood = delta2Sum - priorLogLikelihood;\n+\t\tLOG(Awb, Debug)\n+\t\t\t<< \"Fine search t: \" << tTest\n+\t\t\t<< \" r: \" << rbTest[0]\n+\t\t\t<< \" b: \" << rbTest[1]\n+\t\t\t<< \" offset: \" << bestOffset\n+\t\t\t<< \" likelihood: \" << finalLogLikelihood\n+\t\t\t<< (finalLogLikelihood < bestLogLikelihood ? \" NEW BEST\" : \"\");\n+\n+\t\tif (bestT == 0 || finalLogLikelihood < bestLogLikelihood) {\n+\t\t\tbestLogLikelihood = finalLogLikelihood;\n+\t\t\tbestT = tTest;\n+\t\t\tbestRB = rbTest;\n+\t\t}\n+\t}\n+\n+\tt = bestT;\n+\tr = bestRB[0];\n+\tb = bestRB[1];\n+\tLOG(Awb, Debug)\n+\t\t<< \"Fine search found t \" << t << \" r \" << r << \" b \" << b;\n+}\n+\n+/**\n+ * \\brief Find extremum of function\n+ * \\param[in] a First point\n+ * \\param[in] b Second point\n+ * \\param[in] c Third point\n+ *\n+ * Given 3 points on a curve, find the extremum of the function in that interval\n+ * by fitting a quadratic.\n+ *\n+ * \\return The x value of the extremum clamped to the interval [a.x(), c.x()]\n+ */\n+double AwbBayes::interpolateQuadratic(Pwl::Point const &a, Pwl::Point const &b,\n+\t\t\t\t      Pwl::Point const &c) const\n+{\n+\tconst double eps = 1e-3;\n+\tPwl::Point ca = c - a;\n+\tPwl::Point ba = b - a;\n+\tdouble denominator = 2 * (ba.y() * ca.x() - ca.y() * ba.x());\n+\tif (std::abs(denominator) > eps) {\n+\t\tdouble numerator = ba.y() * ca.x() * ca.x() - ca.y() * ba.x() * ba.x();\n+\t\tdouble result = numerator / denominator + a.x();\n+\t\treturn std::max(a.x(), std::min(c.x(), result));\n+\t}\n+\t/* has degenerated to straight line segment */\n+\treturn a.y() < c.y() - eps ? a.x() : (c.y() < a.y() - eps ? c.x() : b.x());\n+}\n+\n+} /* namespace ipa */\n+\n+} /* namespace libcamera */\ndiff --git a/src/ipa/libipa/awb_bayes.h b/src/ipa/libipa/awb_bayes.h\nnew file mode 100644\nindex 000000000000..5284f0ca4cc0\n--- /dev/null\n+++ b/src/ipa/libipa/awb_bayes.h\n@@ -0,0 +1,67 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024 Ideas on Board Oy\n+ *\n+ * Base class for bayes AWB algorithms\n+ */\n+\n+#pragma once\n+\n+#include <map>\n+#include <memory>\n+#include <tuple>\n+#include <vector>\n+\n+#include <libcamera/base/utils.h>\n+\n+#include <libcamera/control_ids.h>\n+#include <libcamera/controls.h>\n+\n+#include \"libcamera/internal/yaml_parser.h\"\n+\n+#include \"awb.h\"\n+#include \"interpolator.h\"\n+#include \"pwl.h\"\n+#include \"vector.h\"\n+\n+namespace libcamera {\n+\n+namespace ipa {\n+\n+class AwbBayes : public AwbAlgorithm\n+{\n+public:\n+\tAwbBayes() = default;\n+\n+\tint init(const YamlObject &tuningData) override;\n+\tAwbResult calculateAwb(const AwbStats &stats, int lux) override;\n+\tRGB<double> gainsFromColourTemperature(double temperatureK) override;\n+\tvoid handleControls(const ControlList &controls) override;\n+\n+private:\n+\tint readPriors(const YamlObject &tuningData);\n+\n+\tvoid fineSearch(double &t, double &r, double &b, ipa::Pwl const &prior,\n+\t\t\tconst AwbStats &stats) const;\n+\tdouble coarseSearch(const ipa::Pwl &prior, const AwbStats &stats) const;\n+\tdouble interpolateQuadratic(ipa::Pwl::Point const &a,\n+\t\t\t\t    ipa::Pwl::Point const &b,\n+\t\t\t\t    ipa::Pwl::Point const &c) const;\n+\n+\tInterpolator<Pwl> priors_;\n+\tInterpolator<Vector<double, 2>> colourGainCurve_;\n+\n+\tipa::Pwl ctR_;\n+\tipa::Pwl ctB_;\n+\tipa::Pwl ctRInverse_;\n+\tipa::Pwl ctBInverse_;\n+\n+\tdouble transversePos_;\n+\tdouble transverseNeg_;\n+\n+\tModeConfig *currentMode_ = nullptr;\n+};\n+\n+} /* namespace ipa */\n+\n+} /* namespace libcamera */\ndiff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build\nindex c550a6eb45b6..b519c8146d7c 100644\n--- a/src/ipa/libipa/meson.build\n+++ b/src/ipa/libipa/meson.build\n@@ -3,6 +3,7 @@\n libipa_headers = files([\n     'agc_mean_luminance.h',\n     'algorithm.h',\n+    'awb_bayes.h',\n     'awb_grey.h',\n     'awb.h',\n     'camera_sensor_helper.h',\n@@ -22,6 +23,7 @@ libipa_headers = files([\n libipa_sources = files([\n     'agc_mean_luminance.cpp',\n     'algorithm.cpp',\n+    'awb_bayes.cpp',\n     'awb_grey.cpp',\n     'awb.cpp',\n     'camera_sensor_helper.cpp',\n",
    "prefixes": [
        "v1",
        "09/11"
    ]
}