[{"id":37245,"web_url":"https://patchwork.libcamera.org/comment/37245/","msgid":"<CAEmqJPp2VY=c4=+ZHsuUsVSbYcEoQOoVHSmeuS5KMa31X6WQ5w@mail.gmail.com>","date":"2025-12-10T14:01:11","subject":"Re: [PATCH 1/4] ipa: rpi: controller: awb: Separate Bayesian Awb\n\tinto AwbBayes","submitter":{"id":34,"url":"https://patchwork.libcamera.org/api/people/34/","name":"Naushir Patuck","email":"naush@raspberrypi.com"},"content":"Hi David and Peter,\n\nOn Fri, 24 Oct 2025 at 15:41, David Plowman\n<david.plowman@raspberrypi.com> wrote:\n>\n> From: Peter Bailey <peter.bailey@raspberrypi.com>\n>\n> Move parts of the AWB algorithm specific to the Bayesian algorithm into a\n> new class. This will make it easier to add new Awb algorithms in the future.\n>\n> Signed-off-by: Peter Bailey <peter.bailey@raspberrypi.com>\n\nReviewed-by: Naushir Patuck <naush@raspberrypi.com>\n\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\n>\n> diff --git a/src/ipa/rpi/controller/meson.build b/src/ipa/rpi/controller/meson.build\n> index dde4ac12..73c93dca 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',\n> diff --git a/src/ipa/rpi/controller/rpi/awb.cpp b/src/ipa/rpi/controller/rpi/awb.cpp\n> index 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> -       auto value = params[\"lo\"].get<double>();\n> -       if (!value)\n> -               return -EINVAL;\n> -       ctLo = *value;\n> -\n> -       value = params[\"hi\"].get<double>();\n> -       if (!value)\n> -               return -EINVAL;\n> -       ctHi = *value;\n> -\n> -       return 0;\n> -}\n> -\n> -int AwbPrior::read(const libcamera::YamlObject &params)\n> -{\n> -       auto value = params[\"lux\"].get<double>();\n> -       if (!value)\n> -               return -EINVAL;\n> -       lux = *value;\n> -\n> -       prior = params[\"prior\"].get<ipa::Pwl>(ipa::Pwl{});\n> -       return prior.empty() ? -EINVAL : 0;\n> -}\n> -\n>  static int readCtCurve(ipa::Pwl &ctR, ipa::Pwl &ctB, const libcamera::YamlObject &params)\n>  {\n>         if (params.size() % 3) {\n> @@ -92,11 +53,25 @@ static int readCtCurve(ipa::Pwl &ctR, ipa::Pwl &ctB, const libcamera::YamlObject\n>         return 0;\n>  }\n>\n> +int AwbMode::read(const libcamera::YamlObject &params)\n> +{\n> +       auto value = params[\"lo\"].get<double>();\n> +       if (!value)\n> +               return -EINVAL;\n> +       ctLo = *value;\n> +\n> +       value = params[\"hi\"].get<double>();\n> +       if (!value)\n> +               return -EINVAL;\n> +       ctHi = *value;\n> +\n> +       return 0;\n> +}\n> +\n>  int AwbConfig::read(const libcamera::YamlObject &params)\n>  {\n>         int ret;\n>\n> -       bayes = params[\"bayes\"].get<int>(1);\n>         framePeriod = params[\"frame_period\"].get<uint16_t>(10);\n>         startupFrames = params[\"startup_frames\"].get<uint16_t>(10);\n>         convergenceFrames = params[\"convergence_frames\"].get<unsigned int>(3);\n> @@ -111,23 +86,6 @@ int AwbConfig::read(const libcamera::YamlObject &params)\n>                 ctBInverse = ctB.inverse().first;\n>         }\n>\n> -       if (params.contains(\"priors\")) {\n> -               for (const auto &p : params[\"priors\"].asList()) {\n> -                       AwbPrior prior;\n> -                       ret = prior.read(p);\n> -                       if (ret)\n> -                               return ret;\n> -                       if (!priors.empty() && prior.lux <= priors.back().lux) {\n> -                               LOG(RPiAwb, Error) << \"AwbConfig: Prior must be ordered in increasing lux value\";\n> -                               return -EINVAL;\n> -                       }\n> -                       priors.push_back(prior);\n> -               }\n> -               if (priors.empty()) {\n> -                       LOG(RPiAwb, Error) << \"AwbConfig: no AWB priors configured\";\n> -                       return -EINVAL;\n> -               }\n> -       }\n>         if (params.contains(\"modes\")) {\n>                 for (const auto &[key, value] : params[\"modes\"].asDict()) {\n>                         ret = modes[key].read(value);\n> @@ -142,13 +100,10 @@ int AwbConfig::read(const libcamera::YamlObject &params)\n>                 }\n>         }\n>\n> -       minPixels = params[\"min_pixels\"].get<double>(16.0);\n> -       minG = params[\"min_G\"].get<uint16_t>(32);\n> -       minRegions = params[\"min_regions\"].get<uint32_t>(10);\n>         deltaLimit = params[\"delta_limit\"].get<double>(0.2);\n> -       coarseStep = params[\"coarse_step\"].get<double>(0.2);\n>         transversePos = params[\"transverse_pos\"].get<double>(0.01);\n>         transverseNeg = params[\"transverse_neg\"].get<double>(0.01);\n> +\n>         if (transversePos <= 0 || transverseNeg <= 0) {\n>                 LOG(RPiAwb, Error) << \"AwbConfig: transverse_pos/neg must be > 0\";\n>                 return -EINVAL;\n> @@ -157,29 +112,21 @@ int AwbConfig::read(const libcamera::YamlObject &params)\n>         sensitivityR = params[\"sensitivity_r\"].get<double>(1.0);\n>         sensitivityB = params[\"sensitivity_b\"].get<double>(1.0);\n>\n> -       if (bayes) {\n> -               if (ctR.empty() || ctB.empty() || priors.empty() ||\n> -                   defaultMode == nullptr) {\n> -                       LOG(RPiAwb, Warning)\n> -                               << \"Bayesian AWB mis-configured - switch to Grey method\";\n> -                       bayes = false;\n> -               }\n> -       }\n> -       whitepointR = params[\"whitepoint_r\"].get<double>(0.0);\n> -       whitepointB = params[\"whitepoint_b\"].get<double>(0.0);\n> -       if (bayes == false)\n> +       if (hasCtCurve() && defaultMode != nullptr) {\n> +               greyWorld = false;\n> +       } else {\n> +               greyWorld = true;\n>                 sensitivityR = sensitivityB = 1.0; /* nor do sensitivities make any sense */\n> -       /*\n> -        * The biasProportion parameter adds a small proportion of the counted\n> -        * pixles to a region biased to the biasCT colour temperature.\n> -        *\n> -        * A typical value for biasProportion would be between 0.05 to 0.1.\n> -        */\n> -       biasProportion = params[\"bias_proportion\"].get<double>(0.0);\n> -       biasCT = params[\"bias_ct\"].get<double>(kDefaultCT);\n> +       }\n> +\n>         return 0;\n>  }\n>\n> +bool AwbConfig::hasCtCurve() const\n> +{\n> +       return !ctR.empty() && !ctB.empty();\n> +}\n> +\n>  Awb::Awb(Controller *controller)\n>         : AwbAlgorithm(controller)\n>  {\n> @@ -199,16 +146,6 @@ Awb::~Awb()\n>         asyncThread_.join();\n>  }\n>\n> -char const *Awb::name() const\n> -{\n> -       return NAME;\n> -}\n> -\n> -int Awb::read(const libcamera::YamlObject &params)\n> -{\n> -       return config_.read(params);\n> -}\n> -\n>  void Awb::initialise()\n>  {\n>         frameCount_ = framePhase_ = 0;\n> @@ -217,7 +154,7 @@ void Awb::initialise()\n>          * just in case the first few frames don't have anything meaningful in\n>          * them.\n>          */\n> -       if (!config_.ctR.empty() && !config_.ctB.empty()) {\n> +       if (!config_.greyWorld) {\n>                 syncResults_.temperatureK = config_.ctR.domain().clamp(4000);\n>                 syncResults_.gainR = 1.0 / config_.ctR.eval(syncResults_.temperatureK);\n>                 syncResults_.gainG = 1.0;\n> @@ -282,7 +219,7 @@ void Awb::setManualGains(double manualR, double manualB)\n>                 syncResults_.gainR = prevSyncResults_.gainR = manualR_;\n>                 syncResults_.gainG = prevSyncResults_.gainG = 1.0;\n>                 syncResults_.gainB = prevSyncResults_.gainB = manualB_;\n> -               if (config_.bayes) {\n> +               if (!config_.greyWorld) {\n>                         /* Also estimate the best corresponding colour temperature from the curves. */\n>                         double ctR = config_.ctRInverse.eval(config_.ctRInverse.domain().clamp(1 / manualR_));\n>                         double 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> -       if (!config_.bayes) {\n> +       if (config_.greyWorld) {\n>                 LOG(RPiAwb, Warning) << \"AWB uncalibrated - cannot set colour temperature\";\n>                 return;\n>         }\n> @@ -433,10 +370,10 @@ void Awb::asyncFunc()\n>         }\n>  }\n>\n> -static void generateStats(std::vector<Awb::RGB> &zones,\n> -                         StatisticsPtr &stats, double minPixels,\n> -                         double minG, Metadata &globalMetadata,\n> -                         double biasProportion, double biasCtR, double biasCtB)\n> +void Awb::generateStats(std::vector<Awb::RGB> &zones,\n> +                       StatisticsPtr &stats, double minPixels,\n> +                       double minG, Metadata &globalMetadata,\n> +                       double biasProportion, double biasCtR, double biasCtB)\n>  {\n>         std::scoped_lock<RPiController::Metadata> l(globalMetadata);\n>\n> @@ -450,9 +387,9 @@ static void generateStats(std::vector<Awb::RGB> &zones,\n>                         zone.R = region.val.rSum / region.counted;\n>                         zone.B = region.val.bSum / region.counted;\n>                         /*\n> -                        * Add some bias samples to allow the search to tend to a\n> -                        * bias CT in failure cases.\n> -                        */\n> +                       * Add some bias samples to allow the search to tend to a\n> +                       * bias CT in failure cases.\n> +                       */\n>                         const unsigned int proportion = biasProportion * region.counted;\n>                         zone.R += proportion * biasCtR;\n>                         zone.B += proportion * biasCtB;\n> @@ -469,29 +406,7 @@ static void generateStats(std::vector<Awb::RGB> &zones,\n>         }\n>  }\n>\n> -void Awb::prepareStats()\n> -{\n> -       zones_.clear();\n> -       /*\n> -        * LSC has already been applied to the stats in this pipeline, so stop\n> -        * any LSC compensation.  We also ignore config_.fast in this version.\n> -        */\n> -       const double biasCtR = config_.bayes ? config_.ctR.eval(config_.biasCT) : 0;\n> -       const double biasCtB = config_.bayes ? config_.ctB.eval(config_.biasCT) : 0;\n> -       generateStats(zones_, statistics_, config_.minPixels,\n> -                     config_.minG, getGlobalMetadata(),\n> -                     config_.biasProportion, biasCtR, biasCtB);\n> -       /*\n> -        * apply sensitivities, so values appear to come from our \"canonical\"\n> -        * sensor.\n> -        */\n> -       for (auto &zone : zones_) {\n> -               zone.R *= config_.sensitivityR;\n> -               zone.B *= config_.sensitivityB;\n> -       }\n> -}\n> -\n> -double Awb::computeDelta2Sum(double gainR, double gainB)\n> +double Awb::computeDelta2Sum(double gainR, double gainB, double whitepointR, double whitepointB)\n>  {\n>         /*\n>          * 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>          */\n>         double delta2Sum = 0;\n>         for (auto &z : zones_) {\n> -               double deltaR = gainR * z.R - 1 - config_.whitepointR;\n> -               double deltaB = gainB * z.B - 1 - config_.whitepointB;\n> +               double deltaR = gainR * z.R - 1 - whitepointR;\n> +               double deltaB = gainB * z.B - 1 - whitepointB;\n>                 double delta2 = deltaR * deltaR + deltaB * deltaB;\n>                 /* LOG(RPiAwb, Debug) << \"deltaR \" << deltaR << \" deltaB \" << deltaB << \" delta2 \" << delta2; */\n>                 delta2 = std::min(delta2, config_.deltaLimit);\n> @@ -509,39 +424,14 @@ double Awb::computeDelta2Sum(double gainR, double gainB)\n>         return delta2Sum;\n>  }\n>\n> -ipa::Pwl Awb::interpolatePrior()\n> +double Awb::interpolateQuadatric(libcamera::ipa::Pwl::Point const &a,\n> +                                libcamera::ipa::Pwl::Point const &b,\n> +                                libcamera::ipa::Pwl::Point const &c)\n>  {\n>         /*\n> -        * Interpolate the prior log likelihood function for our current lux\n> -        * value.\n> -        */\n> -       if (lux_ <= config_.priors.front().lux)\n> -               return config_.priors.front().prior;\n> -       else if (lux_ >= config_.priors.back().lux)\n> -               return config_.priors.back().prior;\n> -       else {\n> -               int idx = 0;\n> -               /* find which two we lie between */\n> -               while (config_.priors[idx + 1].lux < lux_)\n> -                       idx++;\n> -               double lux0 = config_.priors[idx].lux,\n> -                      lux1 = config_.priors[idx + 1].lux;\n> -               return ipa::Pwl::combine(config_.priors[idx].prior,\n> -                                   config_.priors[idx + 1].prior,\n> -                                   [&](double /*x*/, double y0, double y1) {\n> -                                           return y0 + (y1 - y0) *\n> -                                                       (lux_ - lux0) / (lux1 - lux0);\n> -                                   });\n> -       }\n> -}\n> -\n> -static double interpolateQuadatric(ipa::Pwl::Point const &a, ipa::Pwl::Point const &b,\n> -                                  ipa::Pwl::Point const &c)\n> -{\n> -       /*\n> -        * Given 3 points on a curve, find the extremum of the function in that\n> -        * interval by fitting a quadratic.\n> -        */\n> +       * Given 3 points on a curve, find the extremum of the function in that\n> +       * interval by fitting a quadratic.\n> +       */\n>         const double eps = 1e-3;\n>         ipa::Pwl::Point ca = c - a, ba = b - a;\n>         double 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>         return 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> -       points_.clear(); /* assume doesn't deallocate memory */\n> -       size_t bestPoint = 0;\n> -       double t = mode_->ctLo;\n> -       int spanR = 0, spanB = 0;\n> -       /* Step down the CT curve evaluating log likelihood. */\n> -       while (true) {\n> -               double r = config_.ctR.eval(t, &spanR);\n> -               double b = config_.ctB.eval(t, &spanB);\n> -               double gainR = 1 / r, gainB = 1 / b;\n> -               double delta2Sum = computeDelta2Sum(gainR, gainB);\n> -               double priorLogLikelihood = prior.eval(prior.domain().clamp(t));\n> -               double finalLogLikelihood = delta2Sum - priorLogLikelihood;\n> -               LOG(RPiAwb, Debug)\n> -                       << \"t: \" << t << \" gain R \" << gainR << \" gain B \"\n> -                       << gainB << \" delta2_sum \" << delta2Sum\n> -                       << \" prior \" << priorLogLikelihood << \" final \"\n> -                       << finalLogLikelihood;\n> -               points_.push_back(ipa::Pwl::Point({ t, finalLogLikelihood }));\n> -               if (points_.back().y() < points_[bestPoint].y())\n> -                       bestPoint = points_.size() - 1;\n> -               if (t == mode_->ctHi)\n> -                       break;\n> -               /* for even steps along the r/b curve scale them by the current t */\n> -               t = std::min(t + t / 10 * config_.coarseStep, mode_->ctHi);\n> -       }\n> -       t = points_[bestPoint].x();\n> -       LOG(RPiAwb, Debug) << \"Coarse search found CT \" << t;\n> -       /*\n> -        * We have the best point of the search, but refine it with a quadratic\n> -        * interpolation around its neighbours.\n> -        */\n> -       if (points_.size() > 2) {\n> -               unsigned long bp = std::min(bestPoint, points_.size() - 2);\n> -               bestPoint = std::max(1UL, bp);\n> -               t = interpolateQuadatric(points_[bestPoint - 1],\n> -                                        points_[bestPoint],\n> -                                        points_[bestPoint + 1]);\n> -               LOG(RPiAwb, Debug)\n> -                       << \"After quadratic refinement, coarse search has CT \"\n> -                       << t;\n> -       }\n> -       return t;\n> -}\n> -\n> -void Awb::fineSearch(double &t, double &r, double &b, ipa::Pwl const &prior)\n> -{\n> -       int spanR = -1, spanB = -1;\n> -       config_.ctR.eval(t, &spanR);\n> -       config_.ctB.eval(t, &spanB);\n> -       double step = t / 10 * config_.coarseStep * 0.1;\n> -       int nsteps = 5;\n> -       double rDiff = config_.ctR.eval(t + nsteps * step, &spanR) -\n> -                      config_.ctR.eval(t - nsteps * step, &spanR);\n> -       double bDiff = config_.ctB.eval(t + nsteps * step, &spanB) -\n> -                      config_.ctB.eval(t - nsteps * step, &spanB);\n> -       ipa::Pwl::Point transverse({ bDiff, -rDiff });\n> -       if (transverse.length2() < 1e-6)\n> -               return;\n> -       /*\n> -        * unit vector orthogonal to the b vs. r function (pointing outwards\n> -        * with r and b increasing)\n> -        */\n> -       transverse = transverse / transverse.length();\n> -       double bestLogLikelihood = 0, bestT = 0, bestR = 0, bestB = 0;\n> -       double transverseRange = config_.transverseNeg + config_.transversePos;\n> -       const int maxNumDeltas = 12;\n> -       /* a transverse step approximately every 0.01 r/b units */\n> -       int numDeltas = floor(transverseRange * 100 + 0.5) + 1;\n> -       numDeltas = numDeltas < 3 ? 3 : (numDeltas > maxNumDeltas ? maxNumDeltas : numDeltas);\n> -       /*\n> -        * Step down CT curve. March a bit further if the transverse range is\n> -        * large.\n> -        */\n> -       nsteps += numDeltas;\n> -       for (int i = -nsteps; i <= nsteps; i++) {\n> -               double tTest = t + i * step;\n> -               double priorLogLikelihood =\n> -                       prior.eval(prior.domain().clamp(tTest));\n> -               double rCurve = config_.ctR.eval(tTest, &spanR);\n> -               double bCurve = config_.ctB.eval(tTest, &spanB);\n> -               /* x will be distance off the curve, y the log likelihood there */\n> -               ipa::Pwl::Point points[maxNumDeltas];\n> -               int bestPoint = 0;\n> -               /* Take some measurements transversely *off* the CT curve. */\n> -               for (int j = 0; j < numDeltas; j++) {\n> -                       points[j][0] = -config_.transverseNeg +\n> -                                      (transverseRange * j) / (numDeltas - 1);\n> -                       ipa::Pwl::Point rbTest = ipa::Pwl::Point({ rCurve, bCurve }) +\n> -                                                transverse * points[j].x();\n> -                       double rTest = rbTest.x(), bTest = rbTest.y();\n> -                       double gainR = 1 / rTest, gainB = 1 / bTest;\n> -                       double delta2Sum = computeDelta2Sum(gainR, gainB);\n> -                       points[j][1] = delta2Sum - priorLogLikelihood;\n> -                       LOG(RPiAwb, Debug)\n> -                               << \"At t \" << tTest << \" r \" << rTest << \" b \"\n> -                               << bTest << \": \" << points[j].y();\n> -                       if (points[j].y() < points[bestPoint].y())\n> -                               bestPoint = j;\n> -               }\n> -               /*\n> -                * We have NUM_DELTAS points transversely across the CT curve,\n> -                * now let's do a quadratic interpolation for the best result.\n> -                */\n> -               bestPoint = std::max(1, std::min(bestPoint, numDeltas - 2));\n> -               ipa::Pwl::Point rbTest = ipa::Pwl::Point({ rCurve, bCurve }) +\n> -                                        transverse * interpolateQuadatric(points[bestPoint - 1],\n> -                                                                          points[bestPoint],\n> -                                                                          points[bestPoint + 1]);\n> -               double rTest = rbTest.x(), bTest = rbTest.y();\n> -               double gainR = 1 / rTest, gainB = 1 / bTest;\n> -               double delta2Sum = computeDelta2Sum(gainR, gainB);\n> -               double finalLogLikelihood = delta2Sum - priorLogLikelihood;\n> -               LOG(RPiAwb, Debug)\n> -                       << \"Finally \"\n> -                       << tTest << \" r \" << rTest << \" b \" << bTest << \": \"\n> -                       << finalLogLikelihood\n> -                       << (finalLogLikelihood < bestLogLikelihood ? \" BEST\" : \"\");\n> -               if (bestT == 0 || finalLogLikelihood < bestLogLikelihood)\n> -                       bestLogLikelihood = finalLogLikelihood,\n> -                       bestT = tTest, bestR = rTest, bestB = bTest;\n> -       }\n> -       t = bestT, r = bestR, b = bestB;\n> -       LOG(RPiAwb, Debug)\n> -               << \"Fine search found t \" << t << \" r \" << r << \" b \" << b;\n> -}\n> -\n> -void Awb::awbBayes()\n> -{\n> -       /*\n> -        * May as well divide out G to save computeDelta2Sum from doing it over\n> -        * and over.\n> -        */\n> -       for (auto &z : zones_)\n> -               z.R = z.R / (z.G + 1), z.B = z.B / (z.G + 1);\n> -       /*\n> -        * Get the current prior, and scale according to how many zones are\n> -        * valid... not entirely sure about this.\n> -        */\n> -       ipa::Pwl prior = interpolatePrior();\n> -       prior *= zones_.size() / (double)(statistics_->awbRegions.numRegions());\n> -       prior.map([](double x, double y) {\n> -               LOG(RPiAwb, Debug) << \"(\" << x << \",\" << y << \")\";\n> -       });\n> -       double t = coarseSearch(prior);\n> -       double r = config_.ctR.eval(t);\n> -       double b = config_.ctB.eval(t);\n> -       LOG(RPiAwb, Debug)\n> -               << \"After coarse search: r \" << r << \" b \" << b << \" (gains r \"\n> -               << 1 / r << \" b \" << 1 / b << \")\";\n> -       /*\n> -        * Not entirely sure how to handle the fine search yet. Mostly the\n> -        * estimated CT is already good enough, but the fine search allows us to\n> -        * wander transverely off the CT curve. Under some illuminants, where\n> -        * there may be more or less green light, this may prove beneficial,\n> -        * though I probably need more real datasets before deciding exactly how\n> -        * this should be controlled and tuned.\n> -        */\n> -       fineSearch(t, r, b, prior);\n> -       LOG(RPiAwb, Debug)\n> -               << \"After fine search: r \" << r << \" b \" << b << \" (gains r \"\n> -               << 1 / r << \" b \" << 1 / b << \")\";\n> -       /*\n> -        * Write results out for the main thread to pick up. Remember to adjust\n> -        * the gains from the ones that the \"canonical sensor\" would require to\n> -        * the ones needed by *this* sensor.\n> -        */\n> -       asyncResults_.temperatureK = t;\n> -       asyncResults_.gainR = 1.0 / r * config_.sensitivityR;\n> -       asyncResults_.gainG = 1.0;\n> -       asyncResults_.gainB = 1.0 / b * config_.sensitivityB;\n> -}\n> -\n>  void Awb::awbGrey()\n>  {\n>         LOG(RPiAwb, Debug) << \"Grey world AWB\";\n> @@ -765,32 +481,3 @@ void Awb::awbGrey()\n>         asyncResults_.gainG = 1.0;\n>         asyncResults_.gainB = gainB;\n>  }\n> -\n> -void Awb::doAwb()\n> -{\n> -       prepareStats();\n> -       LOG(RPiAwb, Debug) << \"Valid zones: \" << zones_.size();\n> -       if (zones_.size() > config_.minRegions) {\n> -               if (config_.bayes)\n> -                       awbBayes();\n> -               else\n> -                       awbGrey();\n> -               LOG(RPiAwb, Debug)\n> -                       << \"CT found is \"\n> -                       << asyncResults_.temperatureK\n> -                       << \" with gains r \" << asyncResults_.gainR\n> -                       << \" and b \" << asyncResults_.gainB;\n> -       }\n> -       /*\n> -        * we're done with these; we may as well relinquish our hold on the\n> -        * pointer.\n> -        */\n> -       statistics_.reset();\n> -}\n> -\n> -/* Register algorithm with the system. */\n> -static Algorithm *create(Controller *controller)\n> -{\n> -       return (Algorithm *)new Awb(controller);\n> -}\n> -static RegisterAlgorithm reg(NAME, &create);\n> diff --git a/src/ipa/rpi/controller/rpi/awb.h b/src/ipa/rpi/controller/rpi/awb.h\n> index 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>         int read(const libcamera::YamlObject &params);\n>         double ctLo; /* low CT value for search */\n>         double ctHi; /* high CT value for search */\n>  };\n>\n> -struct AwbPrior {\n> -       int read(const libcamera::YamlObject &params);\n> -       double lux; /* lux level */\n> -       libcamera::ipa::Pwl prior; /* maps CT to prior log likelihood for this lux level */\n> -};\n> -\n>  struct AwbConfig {\n> -       AwbConfig() : defaultMode(nullptr) {}\n> +       AwbConfig()\n> +               : defaultMode(nullptr) {}\n>         int read(const libcamera::YamlObject &params);\n> +       bool hasCtCurve() const;\n> +\n>         /* Only repeat the AWB calculation every \"this many\" frames */\n>         uint16_t framePeriod;\n>         /* number of initial frames for which speed taken as 1.0 (maximum) */\n> @@ -47,27 +38,13 @@ struct AwbConfig {\n>         libcamera::ipa::Pwl ctB; /* function maps CT to b (= B/G) */\n>         libcamera::ipa::Pwl ctRInverse; /* inverse of ctR */\n>         libcamera::ipa::Pwl ctBInverse; /* inverse of ctB */\n> -       /* table of illuminant priors at different lux levels */\n> -       std::vector<AwbPrior> priors;\n> +\n>         /* AWB \"modes\" (determines the search range) */\n>         std::map<std::string, AwbMode> modes;\n>         AwbMode *defaultMode; /* mode used if no mode selected */\n> -       /*\n> -        * minimum proportion of pixels counted within AWB region for it to be\n> -        * \"useful\"\n> -        */\n> -       double minPixels;\n> -       /* minimum G value of those pixels, to be regarded a \"useful\" */\n> -       uint16_t minG;\n> -       /*\n> -        * number of AWB regions that must be \"useful\" in order to do the AWB\n> -        * calculation\n> -        */\n> -       uint32_t minRegions;\n> +\n>         /* clamp on colour error term (so as not to penalise non-grey excessively) */\n>         double deltaLimit;\n> -       /* step size control in coarse search */\n> -       double coarseStep;\n>         /* how far to wander off CT curve towards \"more purple\" */\n>         double transversePos;\n>         /* how far to wander off CT curve towards \"more green\" */\n> @@ -82,14 +59,8 @@ struct AwbConfig {\n>          * sensor's B/G)\n>          */\n>         double sensitivityB;\n> -       /* The whitepoint (which we normally \"aim\" for) can be moved. */\n> -       double whitepointR;\n> -       double whitepointB;\n> -       bool bayes; /* use Bayesian algorithm */\n> -       /* proportion of counted samples to add for the search bias */\n> -       double biasProportion;\n> -       /* CT target for the search bias */\n> -       double biasCT;\n> +\n> +       bool 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>         Awb(Controller *controller = NULL);\n>         ~Awb();\n> -       char const *name() const override;\n> -       void initialise() override;\n> -       int read(const libcamera::YamlObject &params) override;\n> +       virtual void initialise() override;\n>         unsigned int getConvergenceFrames() const override;\n>         void initialValues(double &gainR, double &gainB) override;\n>         void setMode(std::string const &name) override;\n> @@ -110,6 +79,11 @@ public:\n>         void switchMode(CameraMode const &cameraMode, Metadata *metadata) override;\n>         void prepare(Metadata *imageMetadata) override;\n>         void process(StatisticsPtr &stats, Metadata *imageMetadata) override;\n> +\n> +       static double interpolateQuadatric(libcamera::ipa::Pwl::Point const &a,\n> +                                          libcamera::ipa::Pwl::Point const &b,\n> +                                          libcamera::ipa::Pwl::Point const &c);\n> +\n>         struct RGB {\n>                 RGB(double r = 0, double g = 0, double b = 0)\n>                         : R(r), G(g), B(b)\n> @@ -123,10 +97,30 @@ public:\n>                 }\n>         };\n>\n> -private:\n> -       bool isAutoEnabled() const;\n> +protected:\n>         /* configuration is read-only, and available to both threads */\n>         AwbConfig config_;\n> +       /*\n> +        * The following are for the asynchronous thread to use, though the main\n> +        * thread can set/reset them if the async thread is known to be idle:\n> +        */\n> +       std::vector<RGB> zones_;\n> +       StatisticsPtr statistics_;\n> +       double lux_;\n> +       AwbMode *mode_;\n> +       AwbStatus asyncResults_;\n> +\n> +       virtual void doAwb() = 0;\n> +       virtual void prepareStats() = 0;\n> +       double computeDelta2Sum(double gainR, double gainB, double whitepointR, double whitepointB);\n> +       void awbGrey();\n> +       static void generateStats(std::vector<Awb::RGB> &zones,\n> +                                 StatisticsPtr &stats, double minPixels,\n> +                                 double minG, Metadata &globalMetadata,\n> +                                 double biasProportion, double biasCtR, double biasCtB);\n> +\n> +private:\n> +       bool isAutoEnabled() const;\n>         std::thread asyncThread_;\n>         void asyncFunc(); /* asynchronous thread function */\n>         std::mutex mutex_;\n> @@ -152,6 +146,7 @@ private:\n>         AwbStatus syncResults_;\n>         AwbStatus prevSyncResults_;\n>         std::string modeName_;\n> +\n>         /*\n>          * The following are for the asynchronous thread to use, though the main\n>          * thread can set/reset them if the async thread is known to be idle:\n> @@ -159,20 +154,6 @@ private:\n>         void restartAsync(StatisticsPtr &stats, double lux);\n>         /* copy out the results from the async thread so that it can be restarted */\n>         void fetchAsyncResults();\n> -       StatisticsPtr statistics_;\n> -       AwbMode *mode_;\n> -       double lux_;\n> -       AwbStatus asyncResults_;\n> -       void doAwb();\n> -       void awbBayes();\n> -       void awbGrey();\n> -       void prepareStats();\n> -       double computeDelta2Sum(double gainR, double gainB);\n> -       libcamera::ipa::Pwl interpolatePrior();\n> -       double coarseSearch(libcamera::ipa::Pwl const &prior);\n> -       void fineSearch(double &t, double &r, double &b, libcamera::ipa::Pwl const &prior);\n> -       std::vector<RGB> zones_;\n> -       std::vector<libcamera::ipa::Pwl::Point> points_;\n>         /* manual r setting */\n>         double manualR_;\n>         /* manual b setting */\n> @@ -196,4 +177,4 @@ static inline Awb::RGB operator*(Awb::RGB const &rgb, double d)\n>         return d * rgb;\n>  }\n>\n> -} /* namespace RPiController */\n> +} // namespace RPiController\n> diff --git a/src/ipa/rpi/controller/rpi/awb_bayes.cpp b/src/ipa/rpi/controller/rpi/awb_bayes.cpp\n> new file mode 100644\n> index 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> +       int read(const libcamera::YamlObject &params);\n> +       double lux; /* lux level */\n> +       libcamera::ipa::Pwl prior; /* maps CT to prior log likelihood for this lux level */\n> +};\n> +\n> +struct AwbBayesConfig {\n> +       AwbBayesConfig() {}\n> +       int read(const libcamera::YamlObject &params, AwbConfig &config);\n> +       /* table of illuminant priors at different lux levels */\n> +       std::vector<AwbPrior> priors;\n> +       /*\n> +        * minimum proportion of pixels counted within AWB region for it to be\n> +        * \"useful\"\n> +        */\n> +       double minPixels;\n> +       /* minimum G value of those pixels, to be regarded a \"useful\" */\n> +       uint16_t minG;\n> +       /*\n> +        * number of AWB regions that must be \"useful\" in order to do the AWB\n> +        * calculation\n> +        */\n> +       uint32_t minRegions;\n> +       /* step size control in coarse search */\n> +       double coarseStep;\n> +       /* The whitepoint (which we normally \"aim\" for) can be moved. */\n> +       double whitepointR;\n> +       double whitepointB;\n> +       bool bayes; /* use Bayesian algorithm */\n> +       /* proportion of counted samples to add for the search bias */\n> +       double biasProportion;\n> +       /* CT target for the search bias */\n> +       double biasCT;\n> +};\n> +\n> +class AwbBayes : public Awb\n> +{\n> +public:\n> +       AwbBayes(Controller *controller = NULL);\n> +       ~AwbBayes();\n> +       char const *name() const override;\n> +       int read(const libcamera::YamlObject &params) override;\n> +\n> +protected:\n> +       void prepareStats() override;\n> +       void doAwb() override;\n> +\n> +private:\n> +       AwbBayesConfig bayesConfig_;\n> +       void awbBayes();\n> +       libcamera::ipa::Pwl interpolatePrior();\n> +       double coarseSearch(libcamera::ipa::Pwl const &prior);\n> +       void fineSearch(double &t, double &r, double &b, libcamera::ipa::Pwl const &prior);\n> +       std::vector<libcamera::ipa::Pwl::Point> points_;\n> +};\n> +\n> +int AwbPrior::read(const libcamera::YamlObject &params)\n> +{\n> +       auto value = params[\"lux\"].get<double>();\n> +       if (!value)\n> +               return -EINVAL;\n> +       lux = *value;\n> +\n> +       prior = params[\"prior\"].get<ipa::Pwl>(ipa::Pwl{});\n> +       return prior.empty() ? -EINVAL : 0;\n> +}\n> +\n> +int AwbBayesConfig::read(const libcamera::YamlObject &params, AwbConfig &config)\n> +{\n> +       int ret;\n> +\n> +       bayes = params[\"bayes\"].get<int>(1);\n> +\n> +       if (params.contains(\"priors\")) {\n> +               for (const auto &p : params[\"priors\"].asList()) {\n> +                       AwbPrior prior;\n> +                       ret = prior.read(p);\n> +                       if (ret)\n> +                               return ret;\n> +                       if (!priors.empty() && prior.lux <= priors.back().lux) {\n> +                               LOG(RPiAwb, Error) << \"AwbConfig: Prior must be ordered in increasing lux value\";\n> +                               return -EINVAL;\n> +                       }\n> +                       priors.push_back(prior);\n> +               }\n> +               if (priors.empty()) {\n> +                       LOG(RPiAwb, Error) << \"AwbConfig: no AWB priors configured\";\n> +                       return -EINVAL;\n> +               }\n> +       }\n> +\n> +       minPixels = params[\"min_pixels\"].get<double>(16.0);\n> +       minG = params[\"min_G\"].get<uint16_t>(32);\n> +       minRegions = params[\"min_regions\"].get<uint32_t>(10);\n> +       coarseStep = params[\"coarse_step\"].get<double>(0.2);\n> +\n> +       if (bayes) {\n> +               if (!config.hasCtCurve() || priors.empty() ||\n> +                   config.defaultMode == nullptr) {\n> +                       LOG(RPiAwb, Warning)\n> +                               << \"Bayesian AWB mis-configured - switch to Grey method\";\n> +                       bayes = false;\n> +               }\n> +       }\n> +       whitepointR = params[\"whitepoint_r\"].get<double>(0.0);\n> +       whitepointB = params[\"whitepoint_b\"].get<double>(0.0);\n> +       if (bayes == false) {\n> +               config.sensitivityR = config.sensitivityB = 1.0; /* nor do sensitivities make any sense */\n> +               config.greyWorld = true; /* prevent the ct curve being used in manual mode */\n> +       }\n> +       /*\n> +        * The biasProportion parameter adds a small proportion of the counted\n> +        * pixles to a region biased to the biasCT colour temperature.\n> +        *\n> +        * A typical value for biasProportion would be between 0.05 to 0.1.\n> +        */\n> +       biasProportion = params[\"bias_proportion\"].get<double>(0.0);\n> +       biasCT = params[\"bias_ct\"].get<double>(kDefaultCT);\n> +       return 0;\n> +}\n> +\n> +AwbBayes::AwbBayes(Controller *controller)\n> +       : Awb(controller)\n> +{\n> +}\n> +\n> +AwbBayes::~AwbBayes()\n> +{\n> +}\n> +\n> +char const *AwbBayes::name() const\n> +{\n> +       return NAME;\n> +}\n> +\n> +int AwbBayes::read(const libcamera::YamlObject &params)\n> +{\n> +       int ret;\n> +\n> +       ret = config_.read(params);\n> +       if (ret)\n> +               return ret;\n> +\n> +       ret = bayesConfig_.read(params, config_);\n> +       if (ret)\n> +               return ret;\n> +\n> +       return 0;\n> +}\n> +\n> +void AwbBayes::prepareStats()\n> +{\n> +       zones_.clear();\n> +       /*\n> +        * LSC has already been applied to the stats in this pipeline, so stop\n> +        * any LSC compensation.  We also ignore config_.fast in this version.\n> +        */\n> +       const double biasCtR = bayesConfig_.bayes ? config_.ctR.eval(bayesConfig_.biasCT) : 0;\n> +       const double biasCtB = bayesConfig_.bayes ? config_.ctB.eval(bayesConfig_.biasCT) : 0;\n> +       generateStats(zones_, statistics_, bayesConfig_.minPixels,\n> +                     bayesConfig_.minG, getGlobalMetadata(),\n> +                     bayesConfig_.biasProportion, biasCtR, biasCtB);\n> +       /*\n> +        * apply sensitivities, so values appear to come from our \"canonical\"\n> +        * sensor.\n> +        */\n> +       for (auto &zone : zones_) {\n> +               zone.R *= config_.sensitivityR;\n> +               zone.B *= config_.sensitivityB;\n> +       }\n> +}\n> +\n> +ipa::Pwl AwbBayes::interpolatePrior()\n> +{\n> +       /*\n> +        * Interpolate the prior log likelihood function for our current lux\n> +        * value.\n> +        */\n> +       if (lux_ <= bayesConfig_.priors.front().lux)\n> +               return bayesConfig_.priors.front().prior;\n> +       else if (lux_ >= bayesConfig_.priors.back().lux)\n> +               return bayesConfig_.priors.back().prior;\n> +       else {\n> +               int idx = 0;\n> +               /* find which two we lie between */\n> +               while (bayesConfig_.priors[idx + 1].lux < lux_)\n> +                       idx++;\n> +               double lux0 = bayesConfig_.priors[idx].lux,\n> +                      lux1 = bayesConfig_.priors[idx + 1].lux;\n> +               return ipa::Pwl::combine(bayesConfig_.priors[idx].prior,\n> +                                        bayesConfig_.priors[idx + 1].prior,\n> +                                        [&](double /*x*/, double y0, double y1) {\n> +                                                return y0 + (y1 - y0) *\n> +                                                                    (lux_ - lux0) / (lux1 - lux0);\n> +                                        });\n> +       }\n> +}\n> +\n> +double AwbBayes::coarseSearch(ipa::Pwl const &prior)\n> +{\n> +       points_.clear(); /* assume doesn't deallocate memory */\n> +       size_t bestPoint = 0;\n> +       double t = mode_->ctLo;\n> +       int spanR = 0, spanB = 0;\n> +       /* Step down the CT curve evaluating log likelihood. */\n> +       while (true) {\n> +               double r = config_.ctR.eval(t, &spanR);\n> +               double b = config_.ctB.eval(t, &spanB);\n> +               double gainR = 1 / r, gainB = 1 / b;\n> +               double delta2Sum = computeDelta2Sum(gainR, gainB, bayesConfig_.whitepointR, bayesConfig_.whitepointB);\n> +               double priorLogLikelihood = prior.eval(prior.domain().clamp(t));\n> +               double finalLogLikelihood = delta2Sum - priorLogLikelihood;\n> +               LOG(RPiAwb, Debug)\n> +                       << \"t: \" << t << \" gain R \" << gainR << \" gain B \"\n> +                       << gainB << \" delta2_sum \" << delta2Sum\n> +                       << \" prior \" << priorLogLikelihood << \" final \"\n> +                       << finalLogLikelihood;\n> +               points_.push_back(ipa::Pwl::Point({ t, finalLogLikelihood }));\n> +               if (points_.back().y() < points_[bestPoint].y())\n> +                       bestPoint = points_.size() - 1;\n> +               if (t == mode_->ctHi)\n> +                       break;\n> +               /* for even steps along the r/b curve scale them by the current t */\n> +               t = std::min(t + t / 10 * bayesConfig_.coarseStep, mode_->ctHi);\n> +       }\n> +       t = points_[bestPoint].x();\n> +       LOG(RPiAwb, Debug) << \"Coarse search found CT \" << t;\n> +       /*\n> +        * We have the best point of the search, but refine it with a quadratic\n> +        * interpolation around its neighbours.\n> +        */\n> +       if (points_.size() > 2) {\n> +               unsigned long bp = std::min(bestPoint, points_.size() - 2);\n> +               bestPoint = std::max(1UL, bp);\n> +               t = interpolateQuadatric(points_[bestPoint - 1],\n> +                                        points_[bestPoint],\n> +                                        points_[bestPoint + 1]);\n> +               LOG(RPiAwb, Debug)\n> +                       << \"After quadratic refinement, coarse search has CT \"\n> +                       << t;\n> +       }\n> +       return t;\n> +}\n> +\n> +void AwbBayes::fineSearch(double &t, double &r, double &b, ipa::Pwl const &prior)\n> +{\n> +       int spanR = -1, spanB = -1;\n> +       config_.ctR.eval(t, &spanR);\n> +       config_.ctB.eval(t, &spanB);\n> +       double step = t / 10 * bayesConfig_.coarseStep * 0.1;\n> +       int nsteps = 5;\n> +       double rDiff = config_.ctR.eval(t + nsteps * step, &spanR) -\n> +                      config_.ctR.eval(t - nsteps * step, &spanR);\n> +       double bDiff = config_.ctB.eval(t + nsteps * step, &spanB) -\n> +                      config_.ctB.eval(t - nsteps * step, &spanB);\n> +       ipa::Pwl::Point transverse({ bDiff, -rDiff });\n> +       if (transverse.length2() < 1e-6)\n> +               return;\n> +       /*\n> +        * unit vector orthogonal to the b vs. r function (pointing outwards\n> +        * with r and b increasing)\n> +        */\n> +       transverse = transverse / transverse.length();\n> +       double bestLogLikelihood = 0, bestT = 0, bestR = 0, bestB = 0;\n> +       double transverseRange = config_.transverseNeg + config_.transversePos;\n> +       const int maxNumDeltas = 12;\n> +       /* a transverse step approximately every 0.01 r/b units */\n> +       int numDeltas = floor(transverseRange * 100 + 0.5) + 1;\n> +       numDeltas = numDeltas < 3 ? 3 : (numDeltas > maxNumDeltas ? maxNumDeltas : numDeltas);\n> +       /*\n> +        * Step down CT curve. March a bit further if the transverse range is\n> +        * large.\n> +        */\n> +       nsteps += numDeltas;\n> +       for (int i = -nsteps; i <= nsteps; i++) {\n> +               double tTest = t + i * step;\n> +               double priorLogLikelihood =\n> +                       prior.eval(prior.domain().clamp(tTest));\n> +               double rCurve = config_.ctR.eval(tTest, &spanR);\n> +               double bCurve = config_.ctB.eval(tTest, &spanB);\n> +               /* x will be distance off the curve, y the log likelihood there */\n> +               ipa::Pwl::Point points[maxNumDeltas];\n> +               int bestPoint = 0;\n> +               /* Take some measurements transversely *off* the CT curve. */\n> +               for (int j = 0; j < numDeltas; j++) {\n> +                       points[j][0] = -config_.transverseNeg +\n> +                                      (transverseRange * j) / (numDeltas - 1);\n> +                       ipa::Pwl::Point rbTest = ipa::Pwl::Point({ rCurve, bCurve }) +\n> +                                                transverse * points[j].x();\n> +                       double rTest = rbTest.x(), bTest = rbTest.y();\n> +                       double gainR = 1 / rTest, gainB = 1 / bTest;\n> +                       double delta2Sum = computeDelta2Sum(gainR, gainB, bayesConfig_.whitepointR, bayesConfig_.whitepointB);\n> +                       points[j][1] = delta2Sum - priorLogLikelihood;\n> +                       LOG(RPiAwb, Debug)\n> +                               << \"At t \" << tTest << \" r \" << rTest << \" b \"\n> +                               << bTest << \": \" << points[j].y();\n> +                       if (points[j].y() < points[bestPoint].y())\n> +                               bestPoint = j;\n> +               }\n> +               /*\n> +                * We have NUM_DELTAS points transversely across the CT curve,\n> +                * now let's do a quadratic interpolation for the best result.\n> +                */\n> +               bestPoint = std::max(1, std::min(bestPoint, numDeltas - 2));\n> +               ipa::Pwl::Point rbTest = ipa::Pwl::Point({ rCurve, bCurve }) +\n> +                                        transverse * interpolateQuadatric(points[bestPoint - 1],\n> +                                                                          points[bestPoint],\n> +                                                                          points[bestPoint + 1]);\n> +               double rTest = rbTest.x(), bTest = rbTest.y();\n> +               double gainR = 1 / rTest, gainB = 1 / bTest;\n> +               double delta2Sum = computeDelta2Sum(gainR, gainB, bayesConfig_.whitepointR, bayesConfig_.whitepointB);\n> +               double finalLogLikelihood = delta2Sum - priorLogLikelihood;\n> +               LOG(RPiAwb, Debug)\n> +                       << \"Finally \"\n> +                       << tTest << \" r \" << rTest << \" b \" << bTest << \": \"\n> +                       << finalLogLikelihood\n> +                       << (finalLogLikelihood < bestLogLikelihood ? \" BEST\" : \"\");\n> +               if (bestT == 0 || finalLogLikelihood < bestLogLikelihood)\n> +                       bestLogLikelihood = finalLogLikelihood,\n> +                       bestT = tTest, bestR = rTest, bestB = bTest;\n> +       }\n> +       t = bestT, r = bestR, b = bestB;\n> +       LOG(RPiAwb, Debug)\n> +               << \"Fine search found t \" << t << \" r \" << r << \" b \" << b;\n> +}\n> +\n> +void AwbBayes::awbBayes()\n> +{\n> +       /*\n> +        * May as well divide out G to save computeDelta2Sum from doing it over\n> +        * and over.\n> +        */\n> +       for (auto &z : zones_)\n> +               z.R = z.R / (z.G + 1), z.B = z.B / (z.G + 1);\n> +       /*\n> +        * Get the current prior, and scale according to how many zones are\n> +        * valid... not entirely sure about this.\n> +        */\n> +       ipa::Pwl prior = interpolatePrior();\n> +       prior *= zones_.size() / (double)(statistics_->awbRegions.numRegions());\n> +       prior.map([](double x, double y) {\n> +               LOG(RPiAwb, Debug) << \"(\" << x << \",\" << y << \")\";\n> +       });\n> +       double t = coarseSearch(prior);\n> +       double r = config_.ctR.eval(t);\n> +       double b = config_.ctB.eval(t);\n> +       LOG(RPiAwb, Debug)\n> +               << \"After coarse search: r \" << r << \" b \" << b << \" (gains r \"\n> +               << 1 / r << \" b \" << 1 / b << \")\";\n> +       /*\n> +        * Not entirely sure how to handle the fine search yet. Mostly the\n> +        * estimated CT is already good enough, but the fine search allows us to\n> +        * wander transverely off the CT curve. Under some illuminants, where\n> +        * there may be more or less green light, this may prove beneficial,\n> +        * though I probably need more real datasets before deciding exactly how\n> +        * this should be controlled and tuned.\n> +        */\n> +       fineSearch(t, r, b, prior);\n> +       LOG(RPiAwb, Debug)\n> +               << \"After fine search: r \" << r << \" b \" << b << \" (gains r \"\n> +               << 1 / r << \" b \" << 1 / b << \")\";\n> +       /*\n> +        * Write results out for the main thread to pick up. Remember to adjust\n> +        * the gains from the ones that the \"canonical sensor\" would require to\n> +        * the ones needed by *this* sensor.\n> +        */\n> +       asyncResults_.temperatureK = t;\n> +       asyncResults_.gainR = 1.0 / r * config_.sensitivityR;\n> +       asyncResults_.gainG = 1.0;\n> +       asyncResults_.gainB = 1.0 / b * config_.sensitivityB;\n> +}\n> +\n> +void AwbBayes::doAwb()\n> +{\n> +       prepareStats();\n> +       LOG(RPiAwb, Debug) << \"Valid zones: \" << zones_.size();\n> +       if (zones_.size() > bayesConfig_.minRegions) {\n> +               if (bayesConfig_.bayes)\n> +                       awbBayes();\n> +               else\n> +                       awbGrey();\n> +               LOG(RPiAwb, Debug)\n> +                       << \"CT found is \"\n> +                       << asyncResults_.temperatureK\n> +                       << \" with gains r \" << asyncResults_.gainR\n> +                       << \" and b \" << asyncResults_.gainB;\n> +       }\n> +       /*\n> +        * we're done with these; we may as well relinquish our hold on the\n> +        * pointer.\n> +        */\n> +       statistics_.reset();\n> +}\n> +\n> +/* Register algorithm with the system. */\n> +static Algorithm *create(Controller *controller)\n> +{\n> +       return (Algorithm *)new AwbBayes(controller);\n> +}\n> +static RegisterAlgorithm reg(NAME, &create);\n> +\n> +} /* namespace RPiController */\n> --\n> 2.47.3\n>","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 1A134C3257\n\tfor <parsemail@patchwork.libcamera.org>;\n\tWed, 10 Dec 2025 14:01:54 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 44E4061459;\n\tWed, 10 Dec 2025 15:01:53 +0100 (CET)","from mail-vs1-xe35.google.com (mail-vs1-xe35.google.com\n\t[IPv6:2607:f8b0:4864:20::e35])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id DF12F613CB\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 10 Dec 2025 15:01:50 +0100 (CET)","by mail-vs1-xe35.google.com with SMTP id\n\tada2fe7eead31-5dbd4f2c3a3so371008137.1\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 10 Dec 2025 06:01:50 -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=\"QXu9oS2S\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1765375309; x=1765980109;\n\tdarn=lists.libcamera.org; \n\th=cc:to:subject:message-id:date:from:in-reply-to:references\n\t:mime-version:from:to:cc:subject:date:message-id:reply-to;\n\tbh=GfQfuWegSQCDXFU7DPatdK2vqIZ5xUS3UXfDr5r1do8=;\n\tb=QXu9oS2SdBRWoaIgb2ejEp6Os6fqW83ku8sfWc1gbNZ2Cj58CkF5r2A7UJnrGb3Jq/\n\tLHApvmXZSMtBFZzCkKEP7+szLgsfPrq6G1XL+CuK9WMy7N85bEMlKXK5ApAn4QtsggXu\n\toakfdGPM8vd2Azg2hERlwwyFBqmj7+fTT3RmLxyw6flxTqyA2sbXZLCJvk05EcyTwqv6\n\tcrgfiXAWi92/HmKZyMtU/VxrT5ZDZuMIwstVYFsLjjk1kUYEjics8HJ1ZOKpoKooCvmR\n\t74elwZhx15dFbO0BBQZ15GhVxtKuMD5UU0C0J6RliSphxUJGLVJVLQWVQZcVv4KCuQEK\n\tkKGg==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1765375309; x=1765980109;\n\th=cc:to:subject:message-id:date:from:in-reply-to:references\n\t:mime-version:x-gm-gg:x-gm-message-state:from:to:cc:subject:date\n\t:message-id:reply-to;\n\tbh=GfQfuWegSQCDXFU7DPatdK2vqIZ5xUS3UXfDr5r1do8=;\n\tb=b0qXSYQKwjlF0gFKlH4DY/gykJre3fmAdxP7LJ/y2kGgkngNr0Gh5I0sHPtx48Iu2L\n\t2nFO74zoMUSUM1vKI8gItj7Od5lOj40pKXmGvKkkdMrhTLa4hHH3oIHvQqJAqqgjhtAm\n\t/jLKZWveCXhH1SXy0/v/p/fnZntSsYa2KAiYkV/0dah7QIBw1lSODk+h4imKDtVrx9FP\n\tSbdtkXE2yERaKzszJbFhxeWH9iIEV/P7YIEn1xXEOVnOkKKntZ+meQGG41ky1/cRYOzi\n\twtrYSFfQ0bHYgjZaSveG8F+F7laPvpzmO7JMjp+69cD58J1WDEcvUGOZCr8yaO5c4pHD\n\tEkKA==","X-Gm-Message-State":"AOJu0YxiRwZGHv/HQO7AUBHNHR9lEgfriXETB1FcS+02ALdzOUpkbeAO\n\td1vcHcrOum3hXuCer+9LPdRALhBTO+TXShozAmjjk6hag6SnEe5IBYp2gyJ2gu8ik3tanPtidfG\n\tFAa3n0v+P5d0hEtb69vMQUaNEfd/9gg7X1yRvjfMktmuW7QJ/M5jIXZ0=","X-Gm-Gg":"AY/fxX4i8WqiHxs0ug5Ui5ZYKPkKZ7NWzPRnOsM853TTuPFLu/qJCRIARUNkXkh/+aK\n\t/s6ZNwUJZY1loV6iHZ3jWP14mvITNBheSBPN00KXdsXA1uAdDn9tWSL2Pd7uF9C+v/LcsTtFyEq\n\tze0eHAGBC0yuPTzuJ1vsqPqk9VrpbMYjuYXNwC6S2LXFjCIafJqwEFYYFmdOywoG4Q1IvejIO3x\n\tLIAQL8YZZ1ooRSIpPardP3lYkFZilisNdaFi8OXX9NOh6kibrCA6vxT4NW41vDvFhZ6+Iyl97Ix\n\tQBszDyOythrSiGmnwfR0gXTsvLE=","X-Google-Smtp-Source":"AGHT+IGQoFyTr4/BBiWw9PdqUzFXkiO6lNQX/M2YkZCEgB1t/8y2ISbxzfPC6EHXYNr1NXNAiN6vNdn/cdIi72MSXbc=","X-Received":"by 2002:a05:6102:580b:b0:5e5:5cb7:eb11 with SMTP id\n\tada2fe7eead31-5e571a3c1femr388234137.2.1765375308769; Wed, 10 Dec 2025\n\t06:01:48 -0800 (PST)","MIME-Version":"1.0","References":"<20251024144049.3311-1-david.plowman@raspberrypi.com>\n\t<20251024144049.3311-2-david.plowman@raspberrypi.com>","In-Reply-To":"<20251024144049.3311-2-david.plowman@raspberrypi.com>","From":"Naushir Patuck <naush@raspberrypi.com>","Date":"Wed, 10 Dec 2025 14:01:11 +0000","X-Gm-Features":"AQt7F2qr0iDku-DBLiZbHlGDvDP45lKhXDDWImusJTpQ1XzhvBe22cO_X_wcKCM","Message-ID":"<CAEmqJPp2VY=c4=+ZHsuUsVSbYcEoQOoVHSmeuS5KMa31X6WQ5w@mail.gmail.com>","Subject":"Re: [PATCH 1/4] ipa: rpi: controller: awb: Separate Bayesian Awb\n\tinto AwbBayes","To":"David Plowman <david.plowman@raspberrypi.com>","Cc":"libcamera-devel@lists.libcamera.org, \n\tPeter Bailey <peter.bailey@raspberrypi.com>","Content-Type":"text/plain; charset=\"UTF-8\"","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>"}}]