From patchwork Fri Oct 24 14:16:00 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: David Plowman X-Patchwork-Id: 24804 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 4C6FBC3259 for ; Fri, 24 Oct 2025 14:41:02 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id E360B60988; Fri, 24 Oct 2025 16:41:01 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=raspberrypi.com header.i=@raspberrypi.com header.b="doxSXEAj"; dkim-atps=neutral Received: from mail-wr1-x431.google.com (mail-wr1-x431.google.com [IPv6:2a00:1450:4864:20::431]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 3EA1760982 for ; Fri, 24 Oct 2025 16:40:59 +0200 (CEST) Received: by mail-wr1-x431.google.com with SMTP id ffacd0b85a97d-42421b1514fso1455900f8f.2 for ; Fri, 24 Oct 2025 07:40:59 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=raspberrypi.com; s=google; t=1761316858; x=1761921658; darn=lists.libcamera.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=zcuZttP5SgloDxXuKPBlkzTEIYIdJYrXKOASbEfwbcE=; b=doxSXEAjgd+AZlCI+srAKL/CuvQbL2baxQFiEd9qW6eeAmcT3lymJ0nkceRIlNgcFS i7gAbv1o6/oj3rJeI9AFCNKN08nH18GYszvD9P3WhB36cFLPB59nsLI1cz0qoVse8UOW fqvWo27IGNkAAGgMPByQ4SzkD9JjNDOrURTvI/LCS9/eOntxyzuDFvkbwbDYnL1rL/EE y3yEf/f+vaeCO6gNl5ah+fVsof1HFM+W+nBee1nLzWCCJUQ1qfGoggJ8EGP7FAsbfZbC 1Gyc0t501uhGyAWO/77JDIPm8gV/0BHT85x3o1Svti8E5h4x5l8rIZHa+cR3VVyymtEf 9Vcg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1761316858; x=1761921658; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=zcuZttP5SgloDxXuKPBlkzTEIYIdJYrXKOASbEfwbcE=; b=XdI1Osd4GCNxm/MyBCNvQ4y6FmV6d/ImHZr6P8dnmTUrlYSm+GgsBpRNUliRSKYkE+ ECfFU6oxPxqxQQEXdEtWjlQBJpRnszzzl3xZzr7HJOgbTaWx4qRHbzNzfwYB1iU9tGjY QXTokxVQ1m8i4ogzuyYTnzvJ+1WX3/2WdT49ktWnNDDd+xMHgd4gfnaUob3VWBT7f+LW B37CuNGC7GK+sDNWVd5q/Vpz/7WosJqiTNHQo8jXV3aSJFYkiKrxFOYwUh/nSUke41Gm nH4sAY0/jKhqpq4bjpmSBUxEG0Vbedk5P0uEQbzoEHkQXW1pNYo25jC7VbaZ2cGdbEag bl/g== X-Gm-Message-State: AOJu0Yx6GVPJ92SBc2+lMksU1ZJtnI10q8qg7mPYwrFAv+27nqzHftKk BQD+qP3VySBb6BBCavp6p/lXB/P/3MjXLKqn5qrw0I1RGBFLsDYCe0ev+KI2egy8QPAZNfgJnD8 YM5Rz7g8= X-Gm-Gg: ASbGncsZPz/Czu3OiH8dave7HotT020Jv8tdc4L82r28JfjmlCJI+B2MWa9tQIvOVM0 kSgXnf3O/Zy7u1CdMfSFqGuPOmJSY4wRZ1zx7r7CBv9cdv2qsaNJUbX0HM7oLWgeeIe6YhV+Lnd vIX8lHeIzBaSb0IhxWGDqbRsMZShSiUevjntJulc94+SM/Z3y8ElhZPx+MZ9JA6yjVJIiGPYYwq tpvPBF5p+KIQDWJBKqHiIRPof5pACet0XC2FB6e0L1rVWtkz5k27niuw12gIPplQBO11UJs3Kcj QJbyScSBQbAxMh3VB6INs6Xl0Mk5oEiBlUlPO2h5O+k43Yp45OSFro6G2yPpBReEuGdImcSCl+t SY/jUh3pg18oUlNQSbfSoNl1ywntv1zuKUnkV1YVWlnA8vu3TpcaPeeZ9GTN7+iMqj4L4h+Adgb WPkVPByJPDW0eGSIMcONhocWwLnwx9IFdB45ipWfMZtCg4t3eIEf0zlh3nvxU6D8GrTJ6UODvK7 KSxppGoTc+MU3aWTh04MVWjpg== X-Google-Smtp-Source: AGHT+IFaQu8GYNZFAjXjkktCe80zKPGSfCTfF48aIaaOFljzoywcseC4Hc6K7UvhZHJDWveuOiB+gQ== X-Received: by 2002:a05:6000:1a8b:b0:3de:b99d:d43 with SMTP id ffacd0b85a97d-42990708fe6mr2214948f8f.19.1761316857798; Fri, 24 Oct 2025 07:40:57 -0700 (PDT) Received: from localhost.localdomain ([2a06:61c0:f337:0:8aba:2a85:b765:a23e]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-429898ecabdsm9835993f8f.47.2025.10.24.07.40.55 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 24 Oct 2025 07:40:56 -0700 (PDT) From: David Plowman To: libcamera-devel@lists.libcamera.org Cc: Peter Bailey Subject: [PATCH 1/4] ipa: rpi: controller: awb: Separate Bayesian Awb into AwbBayes Date: Fri, 24 Oct 2025 15:16:00 +0100 Message-ID: <20251024144049.3311-2-david.plowman@raspberrypi.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20251024144049.3311-1-david.plowman@raspberrypi.com> References: <20251024144049.3311-1-david.plowman@raspberrypi.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" From: Peter Bailey Move parts of the AWB algorithm specific to the Bayesian algorithm into a new class. This will make it easier to add new Awb algorithms in the future. Signed-off-by: Peter Bailey --- src/ipa/rpi/controller/meson.build | 1 + src/ipa/rpi/controller/rpi/awb.cpp | 409 +++------------------ src/ipa/rpi/controller/rpi/awb.h | 99 ++--- src/ipa/rpi/controller/rpi/awb_bayes.cpp | 444 +++++++++++++++++++++++ 4 files changed, 533 insertions(+), 420 deletions(-) create mode 100644 src/ipa/rpi/controller/rpi/awb_bayes.cpp diff --git a/src/ipa/rpi/controller/meson.build b/src/ipa/rpi/controller/meson.build index dde4ac12..73c93dca 100644 --- a/src/ipa/rpi/controller/meson.build +++ b/src/ipa/rpi/controller/meson.build @@ -10,6 +10,7 @@ rpi_ipa_controller_sources = files([ 'rpi/agc_channel.cpp', 'rpi/alsc.cpp', 'rpi/awb.cpp', + 'rpi/awb_bayes.cpp', 'rpi/black_level.cpp', 'rpi/cac.cpp', 'rpi/ccm.cpp', diff --git a/src/ipa/rpi/controller/rpi/awb.cpp b/src/ipa/rpi/controller/rpi/awb.cpp index 365b595f..de5fa59b 100644 --- a/src/ipa/rpi/controller/rpi/awb.cpp +++ b/src/ipa/rpi/controller/rpi/awb.cpp @@ -1,20 +1,14 @@ /* SPDX-License-Identifier: BSD-2-Clause */ /* - * Copyright (C) 2019, Raspberry Pi Ltd + * Copyright (C) 2025, Raspberry Pi Ltd * * AWB control algorithm */ - -#include -#include -#include - -#include +#include "awb.h" #include "../lux_status.h" #include "alsc_status.h" -#include "awb.h" using namespace RPiController; using namespace libcamera; @@ -23,39 +17,6 @@ LOG_DEFINE_CATEGORY(RPiAwb) constexpr double kDefaultCT = 4500.0; -#define NAME "rpi.awb" - -/* - * todo - the locking in this algorithm needs some tidying up as has been done - * elsewhere (ALSC and AGC). - */ - -int AwbMode::read(const libcamera::YamlObject ¶ms) -{ - auto value = params["lo"].get(); - if (!value) - return -EINVAL; - ctLo = *value; - - value = params["hi"].get(); - if (!value) - return -EINVAL; - ctHi = *value; - - return 0; -} - -int AwbPrior::read(const libcamera::YamlObject ¶ms) -{ - auto value = params["lux"].get(); - if (!value) - return -EINVAL; - lux = *value; - - prior = params["prior"].get(ipa::Pwl{}); - return prior.empty() ? -EINVAL : 0; -} - static int readCtCurve(ipa::Pwl &ctR, ipa::Pwl &ctB, const libcamera::YamlObject ¶ms) { if (params.size() % 3) { @@ -92,11 +53,25 @@ static int readCtCurve(ipa::Pwl &ctR, ipa::Pwl &ctB, const libcamera::YamlObject return 0; } +int AwbMode::read(const libcamera::YamlObject ¶ms) +{ + auto value = params["lo"].get(); + if (!value) + return -EINVAL; + ctLo = *value; + + value = params["hi"].get(); + if (!value) + return -EINVAL; + ctHi = *value; + + return 0; +} + int AwbConfig::read(const libcamera::YamlObject ¶ms) { int ret; - bayes = params["bayes"].get(1); framePeriod = params["frame_period"].get(10); startupFrames = params["startup_frames"].get(10); convergenceFrames = params["convergence_frames"].get(3); @@ -111,23 +86,6 @@ int AwbConfig::read(const libcamera::YamlObject ¶ms) ctBInverse = ctB.inverse().first; } - if (params.contains("priors")) { - for (const auto &p : params["priors"].asList()) { - AwbPrior prior; - ret = prior.read(p); - if (ret) - return ret; - if (!priors.empty() && prior.lux <= priors.back().lux) { - LOG(RPiAwb, Error) << "AwbConfig: Prior must be ordered in increasing lux value"; - return -EINVAL; - } - priors.push_back(prior); - } - if (priors.empty()) { - LOG(RPiAwb, Error) << "AwbConfig: no AWB priors configured"; - return -EINVAL; - } - } if (params.contains("modes")) { for (const auto &[key, value] : params["modes"].asDict()) { ret = modes[key].read(value); @@ -142,13 +100,10 @@ int AwbConfig::read(const libcamera::YamlObject ¶ms) } } - minPixels = params["min_pixels"].get(16.0); - minG = params["min_G"].get(32); - minRegions = params["min_regions"].get(10); deltaLimit = params["delta_limit"].get(0.2); - coarseStep = params["coarse_step"].get(0.2); transversePos = params["transverse_pos"].get(0.01); transverseNeg = params["transverse_neg"].get(0.01); + if (transversePos <= 0 || transverseNeg <= 0) { LOG(RPiAwb, Error) << "AwbConfig: transverse_pos/neg must be > 0"; return -EINVAL; @@ -157,29 +112,21 @@ int AwbConfig::read(const libcamera::YamlObject ¶ms) sensitivityR = params["sensitivity_r"].get(1.0); sensitivityB = params["sensitivity_b"].get(1.0); - if (bayes) { - if (ctR.empty() || ctB.empty() || priors.empty() || - defaultMode == nullptr) { - LOG(RPiAwb, Warning) - << "Bayesian AWB mis-configured - switch to Grey method"; - bayes = false; - } - } - whitepointR = params["whitepoint_r"].get(0.0); - whitepointB = params["whitepoint_b"].get(0.0); - if (bayes == false) + if (hasCtCurve() && defaultMode != nullptr) { + greyWorld = false; + } else { + greyWorld = true; sensitivityR = sensitivityB = 1.0; /* nor do sensitivities make any sense */ - /* - * The biasProportion parameter adds a small proportion of the counted - * pixles to a region biased to the biasCT colour temperature. - * - * A typical value for biasProportion would be between 0.05 to 0.1. - */ - biasProportion = params["bias_proportion"].get(0.0); - biasCT = params["bias_ct"].get(kDefaultCT); + } + return 0; } +bool AwbConfig::hasCtCurve() const +{ + return !ctR.empty() && !ctB.empty(); +} + Awb::Awb(Controller *controller) : AwbAlgorithm(controller) { @@ -199,16 +146,6 @@ Awb::~Awb() asyncThread_.join(); } -char const *Awb::name() const -{ - return NAME; -} - -int Awb::read(const libcamera::YamlObject ¶ms) -{ - return config_.read(params); -} - void Awb::initialise() { frameCount_ = framePhase_ = 0; @@ -217,7 +154,7 @@ void Awb::initialise() * just in case the first few frames don't have anything meaningful in * them. */ - if (!config_.ctR.empty() && !config_.ctB.empty()) { + if (!config_.greyWorld) { syncResults_.temperatureK = config_.ctR.domain().clamp(4000); syncResults_.gainR = 1.0 / config_.ctR.eval(syncResults_.temperatureK); syncResults_.gainG = 1.0; @@ -282,7 +219,7 @@ void Awb::setManualGains(double manualR, double manualB) syncResults_.gainR = prevSyncResults_.gainR = manualR_; syncResults_.gainG = prevSyncResults_.gainG = 1.0; syncResults_.gainB = prevSyncResults_.gainB = manualB_; - if (config_.bayes) { + if (!config_.greyWorld) { /* Also estimate the best corresponding colour temperature from the curves. */ double ctR = config_.ctRInverse.eval(config_.ctRInverse.domain().clamp(1 / manualR_)); double ctB = config_.ctBInverse.eval(config_.ctBInverse.domain().clamp(1 / manualB_)); @@ -294,7 +231,7 @@ void Awb::setManualGains(double manualR, double manualB) void Awb::setColourTemperature(double temperatureK) { - if (!config_.bayes) { + if (config_.greyWorld) { LOG(RPiAwb, Warning) << "AWB uncalibrated - cannot set colour temperature"; return; } @@ -433,10 +370,10 @@ void Awb::asyncFunc() } } -static void generateStats(std::vector &zones, - StatisticsPtr &stats, double minPixels, - double minG, Metadata &globalMetadata, - double biasProportion, double biasCtR, double biasCtB) +void Awb::generateStats(std::vector &zones, + StatisticsPtr &stats, double minPixels, + double minG, Metadata &globalMetadata, + double biasProportion, double biasCtR, double biasCtB) { std::scoped_lock l(globalMetadata); @@ -450,9 +387,9 @@ static void generateStats(std::vector &zones, zone.R = region.val.rSum / region.counted; zone.B = region.val.bSum / region.counted; /* - * Add some bias samples to allow the search to tend to a - * bias CT in failure cases. - */ + * Add some bias samples to allow the search to tend to a + * bias CT in failure cases. + */ const unsigned int proportion = biasProportion * region.counted; zone.R += proportion * biasCtR; zone.B += proportion * biasCtB; @@ -469,29 +406,7 @@ static void generateStats(std::vector &zones, } } -void Awb::prepareStats() -{ - zones_.clear(); - /* - * LSC has already been applied to the stats in this pipeline, so stop - * any LSC compensation. We also ignore config_.fast in this version. - */ - const double biasCtR = config_.bayes ? config_.ctR.eval(config_.biasCT) : 0; - const double biasCtB = config_.bayes ? config_.ctB.eval(config_.biasCT) : 0; - generateStats(zones_, statistics_, config_.minPixels, - config_.minG, getGlobalMetadata(), - config_.biasProportion, biasCtR, biasCtB); - /* - * apply sensitivities, so values appear to come from our "canonical" - * sensor. - */ - for (auto &zone : zones_) { - zone.R *= config_.sensitivityR; - zone.B *= config_.sensitivityB; - } -} - -double Awb::computeDelta2Sum(double gainR, double gainB) +double Awb::computeDelta2Sum(double gainR, double gainB, double whitepointR, double whitepointB) { /* * Compute the sum of the squared colour error (non-greyness) as it @@ -499,8 +414,8 @@ double Awb::computeDelta2Sum(double gainR, double gainB) */ double delta2Sum = 0; for (auto &z : zones_) { - double deltaR = gainR * z.R - 1 - config_.whitepointR; - double deltaB = gainB * z.B - 1 - config_.whitepointB; + double deltaR = gainR * z.R - 1 - whitepointR; + double deltaB = gainB * z.B - 1 - whitepointB; double delta2 = deltaR * deltaR + deltaB * deltaB; /* LOG(RPiAwb, Debug) << "deltaR " << deltaR << " deltaB " << deltaB << " delta2 " << delta2; */ delta2 = std::min(delta2, config_.deltaLimit); @@ -509,39 +424,14 @@ double Awb::computeDelta2Sum(double gainR, double gainB) return delta2Sum; } -ipa::Pwl Awb::interpolatePrior() +double Awb::interpolateQuadatric(libcamera::ipa::Pwl::Point const &a, + libcamera::ipa::Pwl::Point const &b, + libcamera::ipa::Pwl::Point const &c) { /* - * Interpolate the prior log likelihood function for our current lux - * value. - */ - if (lux_ <= config_.priors.front().lux) - return config_.priors.front().prior; - else if (lux_ >= config_.priors.back().lux) - return config_.priors.back().prior; - else { - int idx = 0; - /* find which two we lie between */ - while (config_.priors[idx + 1].lux < lux_) - idx++; - double lux0 = config_.priors[idx].lux, - lux1 = config_.priors[idx + 1].lux; - return ipa::Pwl::combine(config_.priors[idx].prior, - config_.priors[idx + 1].prior, - [&](double /*x*/, double y0, double y1) { - return y0 + (y1 - y0) * - (lux_ - lux0) / (lux1 - lux0); - }); - } -} - -static double interpolateQuadatric(ipa::Pwl::Point const &a, ipa::Pwl::Point const &b, - ipa::Pwl::Point const &c) -{ - /* - * Given 3 points on a curve, find the extremum of the function in that - * interval by fitting a quadratic. - */ + * Given 3 points on a curve, find the extremum of the function in that + * interval by fitting a quadratic. + */ const double eps = 1e-3; ipa::Pwl::Point ca = c - a, ba = b - a; double denominator = 2 * (ba.y() * ca.x() - ca.y() * ba.x()); @@ -554,180 +444,6 @@ static double interpolateQuadatric(ipa::Pwl::Point const &a, ipa::Pwl::Point con return a.y() < c.y() - eps ? a.x() : (c.y() < a.y() - eps ? c.x() : b.x()); } -double Awb::coarseSearch(ipa::Pwl const &prior) -{ - points_.clear(); /* assume doesn't deallocate memory */ - size_t bestPoint = 0; - double t = mode_->ctLo; - int spanR = 0, spanB = 0; - /* Step down the CT curve evaluating log likelihood. */ - while (true) { - double r = config_.ctR.eval(t, &spanR); - double b = config_.ctB.eval(t, &spanB); - double gainR = 1 / r, gainB = 1 / b; - double delta2Sum = computeDelta2Sum(gainR, gainB); - double priorLogLikelihood = prior.eval(prior.domain().clamp(t)); - double finalLogLikelihood = delta2Sum - priorLogLikelihood; - LOG(RPiAwb, Debug) - << "t: " << t << " gain R " << gainR << " gain B " - << gainB << " delta2_sum " << delta2Sum - << " prior " << priorLogLikelihood << " final " - << finalLogLikelihood; - points_.push_back(ipa::Pwl::Point({ t, finalLogLikelihood })); - if (points_.back().y() < points_[bestPoint].y()) - bestPoint = points_.size() - 1; - if (t == mode_->ctHi) - break; - /* for even steps along the r/b curve scale them by the current t */ - t = std::min(t + t / 10 * config_.coarseStep, mode_->ctHi); - } - t = points_[bestPoint].x(); - LOG(RPiAwb, Debug) << "Coarse search found CT " << t; - /* - * We have the best point of the search, but refine it with a quadratic - * interpolation around its neighbours. - */ - if (points_.size() > 2) { - unsigned long bp = std::min(bestPoint, points_.size() - 2); - bestPoint = std::max(1UL, bp); - t = interpolateQuadatric(points_[bestPoint - 1], - points_[bestPoint], - points_[bestPoint + 1]); - LOG(RPiAwb, Debug) - << "After quadratic refinement, coarse search has CT " - << t; - } - return t; -} - -void Awb::fineSearch(double &t, double &r, double &b, ipa::Pwl const &prior) -{ - int spanR = -1, spanB = -1; - config_.ctR.eval(t, &spanR); - config_.ctB.eval(t, &spanB); - double step = t / 10 * config_.coarseStep * 0.1; - int nsteps = 5; - double rDiff = config_.ctR.eval(t + nsteps * step, &spanR) - - config_.ctR.eval(t - nsteps * step, &spanR); - double bDiff = config_.ctB.eval(t + nsteps * step, &spanB) - - config_.ctB.eval(t - nsteps * step, &spanB); - ipa::Pwl::Point transverse({ bDiff, -rDiff }); - if (transverse.length2() < 1e-6) - return; - /* - * unit vector orthogonal to the b vs. r function (pointing outwards - * with r and b increasing) - */ - transverse = transverse / transverse.length(); - double bestLogLikelihood = 0, bestT = 0, bestR = 0, bestB = 0; - double transverseRange = config_.transverseNeg + config_.transversePos; - const int maxNumDeltas = 12; - /* a transverse step approximately every 0.01 r/b units */ - int numDeltas = floor(transverseRange * 100 + 0.5) + 1; - numDeltas = numDeltas < 3 ? 3 : (numDeltas > maxNumDeltas ? maxNumDeltas : numDeltas); - /* - * Step down CT curve. March a bit further if the transverse range is - * large. - */ - nsteps += numDeltas; - for (int i = -nsteps; i <= nsteps; i++) { - double tTest = t + i * step; - double priorLogLikelihood = - prior.eval(prior.domain().clamp(tTest)); - double rCurve = config_.ctR.eval(tTest, &spanR); - double bCurve = config_.ctB.eval(tTest, &spanB); - /* x will be distance off the curve, y the log likelihood there */ - ipa::Pwl::Point points[maxNumDeltas]; - int bestPoint = 0; - /* Take some measurements transversely *off* the CT curve. */ - for (int j = 0; j < numDeltas; j++) { - points[j][0] = -config_.transverseNeg + - (transverseRange * j) / (numDeltas - 1); - ipa::Pwl::Point rbTest = ipa::Pwl::Point({ rCurve, bCurve }) + - transverse * points[j].x(); - double rTest = rbTest.x(), bTest = rbTest.y(); - double gainR = 1 / rTest, gainB = 1 / bTest; - double delta2Sum = computeDelta2Sum(gainR, gainB); - points[j][1] = delta2Sum - priorLogLikelihood; - LOG(RPiAwb, Debug) - << "At t " << tTest << " r " << rTest << " b " - << bTest << ": " << points[j].y(); - if (points[j].y() < points[bestPoint].y()) - bestPoint = j; - } - /* - * We have NUM_DELTAS points transversely across the CT curve, - * now let's do a quadratic interpolation for the best result. - */ - bestPoint = std::max(1, std::min(bestPoint, numDeltas - 2)); - ipa::Pwl::Point rbTest = ipa::Pwl::Point({ rCurve, bCurve }) + - transverse * interpolateQuadatric(points[bestPoint - 1], - points[bestPoint], - points[bestPoint + 1]); - double rTest = rbTest.x(), bTest = rbTest.y(); - double gainR = 1 / rTest, gainB = 1 / bTest; - double delta2Sum = computeDelta2Sum(gainR, gainB); - double finalLogLikelihood = delta2Sum - priorLogLikelihood; - LOG(RPiAwb, Debug) - << "Finally " - << tTest << " r " << rTest << " b " << bTest << ": " - << finalLogLikelihood - << (finalLogLikelihood < bestLogLikelihood ? " BEST" : ""); - if (bestT == 0 || finalLogLikelihood < bestLogLikelihood) - bestLogLikelihood = finalLogLikelihood, - bestT = tTest, bestR = rTest, bestB = bTest; - } - t = bestT, r = bestR, b = bestB; - LOG(RPiAwb, Debug) - << "Fine search found t " << t << " r " << r << " b " << b; -} - -void Awb::awbBayes() -{ - /* - * May as well divide out G to save computeDelta2Sum from doing it over - * and over. - */ - for (auto &z : zones_) - z.R = z.R / (z.G + 1), z.B = z.B / (z.G + 1); - /* - * Get the current prior, and scale according to how many zones are - * valid... not entirely sure about this. - */ - ipa::Pwl prior = interpolatePrior(); - prior *= zones_.size() / (double)(statistics_->awbRegions.numRegions()); - prior.map([](double x, double y) { - LOG(RPiAwb, Debug) << "(" << x << "," << y << ")"; - }); - double t = coarseSearch(prior); - double r = config_.ctR.eval(t); - double b = config_.ctB.eval(t); - LOG(RPiAwb, Debug) - << "After coarse search: r " << r << " b " << b << " (gains r " - << 1 / r << " b " << 1 / b << ")"; - /* - * Not entirely sure how to handle the fine search yet. Mostly the - * estimated CT is already good enough, but the fine search allows us to - * wander transverely off the CT curve. Under some illuminants, where - * there may be more or less green light, this may prove beneficial, - * though I probably need more real datasets before deciding exactly how - * this should be controlled and tuned. - */ - fineSearch(t, r, b, prior); - LOG(RPiAwb, Debug) - << "After fine search: r " << r << " b " << b << " (gains r " - << 1 / r << " b " << 1 / b << ")"; - /* - * Write results out for the main thread to pick up. Remember to adjust - * the gains from the ones that the "canonical sensor" would require to - * the ones needed by *this* sensor. - */ - asyncResults_.temperatureK = t; - asyncResults_.gainR = 1.0 / r * config_.sensitivityR; - asyncResults_.gainG = 1.0; - asyncResults_.gainB = 1.0 / b * config_.sensitivityB; -} - void Awb::awbGrey() { LOG(RPiAwb, Debug) << "Grey world AWB"; @@ -765,32 +481,3 @@ void Awb::awbGrey() asyncResults_.gainG = 1.0; asyncResults_.gainB = gainB; } - -void Awb::doAwb() -{ - prepareStats(); - LOG(RPiAwb, Debug) << "Valid zones: " << zones_.size(); - if (zones_.size() > config_.minRegions) { - if (config_.bayes) - awbBayes(); - else - awbGrey(); - LOG(RPiAwb, Debug) - << "CT found is " - << asyncResults_.temperatureK - << " with gains r " << asyncResults_.gainR - << " and b " << asyncResults_.gainB; - } - /* - * we're done with these; we may as well relinquish our hold on the - * pointer. - */ - statistics_.reset(); -} - -/* Register algorithm with the system. */ -static Algorithm *create(Controller *controller) -{ - return (Algorithm *)new Awb(controller); -} -static RegisterAlgorithm reg(NAME, &create); diff --git a/src/ipa/rpi/controller/rpi/awb.h b/src/ipa/rpi/controller/rpi/awb.h index 2fb91254..8b2d8d1d 100644 --- a/src/ipa/rpi/controller/rpi/awb.h +++ b/src/ipa/rpi/controller/rpi/awb.h @@ -1,42 +1,33 @@ /* SPDX-License-Identifier: BSD-2-Clause */ /* - * Copyright (C) 2019, Raspberry Pi Ltd + * Copyright (C) 2025, Raspberry Pi Ltd * * AWB control algorithm */ #pragma once -#include #include +#include #include -#include - #include "../awb_algorithm.h" #include "../awb_status.h" -#include "../statistics.h" - #include "libipa/pwl.h" namespace RPiController { -/* Control algorithm to perform AWB calculations. */ - struct AwbMode { int read(const libcamera::YamlObject ¶ms); double ctLo; /* low CT value for search */ double ctHi; /* high CT value for search */ }; -struct AwbPrior { - int read(const libcamera::YamlObject ¶ms); - double lux; /* lux level */ - libcamera::ipa::Pwl prior; /* maps CT to prior log likelihood for this lux level */ -}; - struct AwbConfig { - AwbConfig() : defaultMode(nullptr) {} + AwbConfig() + : defaultMode(nullptr) {} int read(const libcamera::YamlObject ¶ms); + bool hasCtCurve() const; + /* Only repeat the AWB calculation every "this many" frames */ uint16_t framePeriod; /* number of initial frames for which speed taken as 1.0 (maximum) */ @@ -47,27 +38,13 @@ struct AwbConfig { libcamera::ipa::Pwl ctB; /* function maps CT to b (= B/G) */ libcamera::ipa::Pwl ctRInverse; /* inverse of ctR */ libcamera::ipa::Pwl ctBInverse; /* inverse of ctB */ - /* table of illuminant priors at different lux levels */ - std::vector priors; + /* AWB "modes" (determines the search range) */ std::map modes; AwbMode *defaultMode; /* mode used if no mode selected */ - /* - * minimum proportion of pixels counted within AWB region for it to be - * "useful" - */ - double minPixels; - /* minimum G value of those pixels, to be regarded a "useful" */ - uint16_t minG; - /* - * number of AWB regions that must be "useful" in order to do the AWB - * calculation - */ - uint32_t minRegions; + /* clamp on colour error term (so as not to penalise non-grey excessively) */ double deltaLimit; - /* step size control in coarse search */ - double coarseStep; /* how far to wander off CT curve towards "more purple" */ double transversePos; /* how far to wander off CT curve towards "more green" */ @@ -82,14 +59,8 @@ struct AwbConfig { * sensor's B/G) */ double sensitivityB; - /* The whitepoint (which we normally "aim" for) can be moved. */ - double whitepointR; - double whitepointB; - bool bayes; /* use Bayesian algorithm */ - /* proportion of counted samples to add for the search bias */ - double biasProportion; - /* CT target for the search bias */ - double biasCT; + + bool greyWorld; /* don't use the ct curve when in grey world mode */ }; class Awb : public AwbAlgorithm @@ -97,9 +68,7 @@ class Awb : public AwbAlgorithm public: Awb(Controller *controller = NULL); ~Awb(); - char const *name() const override; - void initialise() override; - int read(const libcamera::YamlObject ¶ms) override; + virtual void initialise() override; unsigned int getConvergenceFrames() const override; void initialValues(double &gainR, double &gainB) override; void setMode(std::string const &name) override; @@ -110,6 +79,11 @@ public: void switchMode(CameraMode const &cameraMode, Metadata *metadata) override; void prepare(Metadata *imageMetadata) override; void process(StatisticsPtr &stats, Metadata *imageMetadata) override; + + static double interpolateQuadatric(libcamera::ipa::Pwl::Point const &a, + libcamera::ipa::Pwl::Point const &b, + libcamera::ipa::Pwl::Point const &c); + struct RGB { RGB(double r = 0, double g = 0, double b = 0) : R(r), G(g), B(b) @@ -123,10 +97,30 @@ public: } }; -private: - bool isAutoEnabled() const; +protected: /* configuration is read-only, and available to both threads */ AwbConfig config_; + /* + * The following are for the asynchronous thread to use, though the main + * thread can set/reset them if the async thread is known to be idle: + */ + std::vector zones_; + StatisticsPtr statistics_; + double lux_; + AwbMode *mode_; + AwbStatus asyncResults_; + + virtual void doAwb() = 0; + virtual void prepareStats() = 0; + double computeDelta2Sum(double gainR, double gainB, double whitepointR, double whitepointB); + void awbGrey(); + static void generateStats(std::vector &zones, + StatisticsPtr &stats, double minPixels, + double minG, Metadata &globalMetadata, + double biasProportion, double biasCtR, double biasCtB); + +private: + bool isAutoEnabled() const; std::thread asyncThread_; void asyncFunc(); /* asynchronous thread function */ std::mutex mutex_; @@ -152,6 +146,7 @@ private: AwbStatus syncResults_; AwbStatus prevSyncResults_; std::string modeName_; + /* * The following are for the asynchronous thread to use, though the main * thread can set/reset them if the async thread is known to be idle: @@ -159,20 +154,6 @@ private: void restartAsync(StatisticsPtr &stats, double lux); /* copy out the results from the async thread so that it can be restarted */ void fetchAsyncResults(); - StatisticsPtr statistics_; - AwbMode *mode_; - double lux_; - AwbStatus asyncResults_; - void doAwb(); - void awbBayes(); - void awbGrey(); - void prepareStats(); - double computeDelta2Sum(double gainR, double gainB); - libcamera::ipa::Pwl interpolatePrior(); - double coarseSearch(libcamera::ipa::Pwl const &prior); - void fineSearch(double &t, double &r, double &b, libcamera::ipa::Pwl const &prior); - std::vector zones_; - std::vector points_; /* manual r setting */ double manualR_; /* manual b setting */ @@ -196,4 +177,4 @@ static inline Awb::RGB operator*(Awb::RGB const &rgb, double d) return d * rgb; } -} /* namespace RPiController */ +} // namespace RPiController diff --git a/src/ipa/rpi/controller/rpi/awb_bayes.cpp b/src/ipa/rpi/controller/rpi/awb_bayes.cpp new file mode 100644 index 00000000..09233cec --- /dev/null +++ b/src/ipa/rpi/controller/rpi/awb_bayes.cpp @@ -0,0 +1,444 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* + * Copyright (C) 2019, Raspberry Pi Ltd + * + * AWB control algorithm + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "../awb_algorithm.h" +#include "../awb_status.h" +#include "../lux_status.h" +#include "../statistics.h" +#include "libipa/pwl.h" + +#include "alsc_status.h" +#include "awb.h" + +using namespace libcamera; + +LOG_DECLARE_CATEGORY(RPiAwb) + +constexpr double kDefaultCT = 4500.0; + +#define NAME "rpi.awb" + +/* + * todo - the locking in this algorithm needs some tidying up as has been done + * elsewhere (ALSC and AGC). + */ + +namespace RPiController { + +struct AwbPrior { + int read(const libcamera::YamlObject ¶ms); + double lux; /* lux level */ + libcamera::ipa::Pwl prior; /* maps CT to prior log likelihood for this lux level */ +}; + +struct AwbBayesConfig { + AwbBayesConfig() {} + int read(const libcamera::YamlObject ¶ms, AwbConfig &config); + /* table of illuminant priors at different lux levels */ + std::vector priors; + /* + * minimum proportion of pixels counted within AWB region for it to be + * "useful" + */ + double minPixels; + /* minimum G value of those pixels, to be regarded a "useful" */ + uint16_t minG; + /* + * number of AWB regions that must be "useful" in order to do the AWB + * calculation + */ + uint32_t minRegions; + /* step size control in coarse search */ + double coarseStep; + /* The whitepoint (which we normally "aim" for) can be moved. */ + double whitepointR; + double whitepointB; + bool bayes; /* use Bayesian algorithm */ + /* proportion of counted samples to add for the search bias */ + double biasProportion; + /* CT target for the search bias */ + double biasCT; +}; + +class AwbBayes : public Awb +{ +public: + AwbBayes(Controller *controller = NULL); + ~AwbBayes(); + char const *name() const override; + int read(const libcamera::YamlObject ¶ms) override; + +protected: + void prepareStats() override; + void doAwb() override; + +private: + AwbBayesConfig bayesConfig_; + void awbBayes(); + libcamera::ipa::Pwl interpolatePrior(); + double coarseSearch(libcamera::ipa::Pwl const &prior); + void fineSearch(double &t, double &r, double &b, libcamera::ipa::Pwl const &prior); + std::vector points_; +}; + +int AwbPrior::read(const libcamera::YamlObject ¶ms) +{ + auto value = params["lux"].get(); + if (!value) + return -EINVAL; + lux = *value; + + prior = params["prior"].get(ipa::Pwl{}); + return prior.empty() ? -EINVAL : 0; +} + +int AwbBayesConfig::read(const libcamera::YamlObject ¶ms, AwbConfig &config) +{ + int ret; + + bayes = params["bayes"].get(1); + + if (params.contains("priors")) { + for (const auto &p : params["priors"].asList()) { + AwbPrior prior; + ret = prior.read(p); + if (ret) + return ret; + if (!priors.empty() && prior.lux <= priors.back().lux) { + LOG(RPiAwb, Error) << "AwbConfig: Prior must be ordered in increasing lux value"; + return -EINVAL; + } + priors.push_back(prior); + } + if (priors.empty()) { + LOG(RPiAwb, Error) << "AwbConfig: no AWB priors configured"; + return -EINVAL; + } + } + + minPixels = params["min_pixels"].get(16.0); + minG = params["min_G"].get(32); + minRegions = params["min_regions"].get(10); + coarseStep = params["coarse_step"].get(0.2); + + if (bayes) { + if (!config.hasCtCurve() || priors.empty() || + config.defaultMode == nullptr) { + LOG(RPiAwb, Warning) + << "Bayesian AWB mis-configured - switch to Grey method"; + bayes = false; + } + } + whitepointR = params["whitepoint_r"].get(0.0); + whitepointB = params["whitepoint_b"].get(0.0); + if (bayes == false) { + config.sensitivityR = config.sensitivityB = 1.0; /* nor do sensitivities make any sense */ + config.greyWorld = true; /* prevent the ct curve being used in manual mode */ + } + /* + * The biasProportion parameter adds a small proportion of the counted + * pixles to a region biased to the biasCT colour temperature. + * + * A typical value for biasProportion would be between 0.05 to 0.1. + */ + biasProportion = params["bias_proportion"].get(0.0); + biasCT = params["bias_ct"].get(kDefaultCT); + return 0; +} + +AwbBayes::AwbBayes(Controller *controller) + : Awb(controller) +{ +} + +AwbBayes::~AwbBayes() +{ +} + +char const *AwbBayes::name() const +{ + return NAME; +} + +int AwbBayes::read(const libcamera::YamlObject ¶ms) +{ + int ret; + + ret = config_.read(params); + if (ret) + return ret; + + ret = bayesConfig_.read(params, config_); + if (ret) + return ret; + + return 0; +} + +void AwbBayes::prepareStats() +{ + zones_.clear(); + /* + * LSC has already been applied to the stats in this pipeline, so stop + * any LSC compensation. We also ignore config_.fast in this version. + */ + const double biasCtR = bayesConfig_.bayes ? config_.ctR.eval(bayesConfig_.biasCT) : 0; + const double biasCtB = bayesConfig_.bayes ? config_.ctB.eval(bayesConfig_.biasCT) : 0; + generateStats(zones_, statistics_, bayesConfig_.minPixels, + bayesConfig_.minG, getGlobalMetadata(), + bayesConfig_.biasProportion, biasCtR, biasCtB); + /* + * apply sensitivities, so values appear to come from our "canonical" + * sensor. + */ + for (auto &zone : zones_) { + zone.R *= config_.sensitivityR; + zone.B *= config_.sensitivityB; + } +} + +ipa::Pwl AwbBayes::interpolatePrior() +{ + /* + * Interpolate the prior log likelihood function for our current lux + * value. + */ + if (lux_ <= bayesConfig_.priors.front().lux) + return bayesConfig_.priors.front().prior; + else if (lux_ >= bayesConfig_.priors.back().lux) + return bayesConfig_.priors.back().prior; + else { + int idx = 0; + /* find which two we lie between */ + while (bayesConfig_.priors[idx + 1].lux < lux_) + idx++; + double lux0 = bayesConfig_.priors[idx].lux, + lux1 = bayesConfig_.priors[idx + 1].lux; + return ipa::Pwl::combine(bayesConfig_.priors[idx].prior, + bayesConfig_.priors[idx + 1].prior, + [&](double /*x*/, double y0, double y1) { + return y0 + (y1 - y0) * + (lux_ - lux0) / (lux1 - lux0); + }); + } +} + +double AwbBayes::coarseSearch(ipa::Pwl const &prior) +{ + points_.clear(); /* assume doesn't deallocate memory */ + size_t bestPoint = 0; + double t = mode_->ctLo; + int spanR = 0, spanB = 0; + /* Step down the CT curve evaluating log likelihood. */ + while (true) { + double r = config_.ctR.eval(t, &spanR); + double b = config_.ctB.eval(t, &spanB); + double gainR = 1 / r, gainB = 1 / b; + double delta2Sum = computeDelta2Sum(gainR, gainB, bayesConfig_.whitepointR, bayesConfig_.whitepointB); + double priorLogLikelihood = prior.eval(prior.domain().clamp(t)); + double finalLogLikelihood = delta2Sum - priorLogLikelihood; + LOG(RPiAwb, Debug) + << "t: " << t << " gain R " << gainR << " gain B " + << gainB << " delta2_sum " << delta2Sum + << " prior " << priorLogLikelihood << " final " + << finalLogLikelihood; + points_.push_back(ipa::Pwl::Point({ t, finalLogLikelihood })); + if (points_.back().y() < points_[bestPoint].y()) + bestPoint = points_.size() - 1; + if (t == mode_->ctHi) + break; + /* for even steps along the r/b curve scale them by the current t */ + t = std::min(t + t / 10 * bayesConfig_.coarseStep, mode_->ctHi); + } + t = points_[bestPoint].x(); + LOG(RPiAwb, Debug) << "Coarse search found CT " << t; + /* + * We have the best point of the search, but refine it with a quadratic + * interpolation around its neighbours. + */ + if (points_.size() > 2) { + unsigned long bp = std::min(bestPoint, points_.size() - 2); + bestPoint = std::max(1UL, bp); + t = interpolateQuadatric(points_[bestPoint - 1], + points_[bestPoint], + points_[bestPoint + 1]); + LOG(RPiAwb, Debug) + << "After quadratic refinement, coarse search has CT " + << t; + } + return t; +} + +void AwbBayes::fineSearch(double &t, double &r, double &b, ipa::Pwl const &prior) +{ + int spanR = -1, spanB = -1; + config_.ctR.eval(t, &spanR); + config_.ctB.eval(t, &spanB); + double step = t / 10 * bayesConfig_.coarseStep * 0.1; + int nsteps = 5; + double rDiff = config_.ctR.eval(t + nsteps * step, &spanR) - + config_.ctR.eval(t - nsteps * step, &spanR); + double bDiff = config_.ctB.eval(t + nsteps * step, &spanB) - + config_.ctB.eval(t - nsteps * step, &spanB); + ipa::Pwl::Point transverse({ bDiff, -rDiff }); + if (transverse.length2() < 1e-6) + return; + /* + * unit vector orthogonal to the b vs. r function (pointing outwards + * with r and b increasing) + */ + transverse = transverse / transverse.length(); + double bestLogLikelihood = 0, bestT = 0, bestR = 0, bestB = 0; + double transverseRange = config_.transverseNeg + config_.transversePos; + const int maxNumDeltas = 12; + /* a transverse step approximately every 0.01 r/b units */ + int numDeltas = floor(transverseRange * 100 + 0.5) + 1; + numDeltas = numDeltas < 3 ? 3 : (numDeltas > maxNumDeltas ? maxNumDeltas : numDeltas); + /* + * Step down CT curve. March a bit further if the transverse range is + * large. + */ + nsteps += numDeltas; + for (int i = -nsteps; i <= nsteps; i++) { + double tTest = t + i * step; + double priorLogLikelihood = + prior.eval(prior.domain().clamp(tTest)); + double rCurve = config_.ctR.eval(tTest, &spanR); + double bCurve = config_.ctB.eval(tTest, &spanB); + /* x will be distance off the curve, y the log likelihood there */ + ipa::Pwl::Point points[maxNumDeltas]; + int bestPoint = 0; + /* Take some measurements transversely *off* the CT curve. */ + for (int j = 0; j < numDeltas; j++) { + points[j][0] = -config_.transverseNeg + + (transverseRange * j) / (numDeltas - 1); + ipa::Pwl::Point rbTest = ipa::Pwl::Point({ rCurve, bCurve }) + + transverse * points[j].x(); + double rTest = rbTest.x(), bTest = rbTest.y(); + double gainR = 1 / rTest, gainB = 1 / bTest; + double delta2Sum = computeDelta2Sum(gainR, gainB, bayesConfig_.whitepointR, bayesConfig_.whitepointB); + points[j][1] = delta2Sum - priorLogLikelihood; + LOG(RPiAwb, Debug) + << "At t " << tTest << " r " << rTest << " b " + << bTest << ": " << points[j].y(); + if (points[j].y() < points[bestPoint].y()) + bestPoint = j; + } + /* + * We have NUM_DELTAS points transversely across the CT curve, + * now let's do a quadratic interpolation for the best result. + */ + bestPoint = std::max(1, std::min(bestPoint, numDeltas - 2)); + ipa::Pwl::Point rbTest = ipa::Pwl::Point({ rCurve, bCurve }) + + transverse * interpolateQuadatric(points[bestPoint - 1], + points[bestPoint], + points[bestPoint + 1]); + double rTest = rbTest.x(), bTest = rbTest.y(); + double gainR = 1 / rTest, gainB = 1 / bTest; + double delta2Sum = computeDelta2Sum(gainR, gainB, bayesConfig_.whitepointR, bayesConfig_.whitepointB); + double finalLogLikelihood = delta2Sum - priorLogLikelihood; + LOG(RPiAwb, Debug) + << "Finally " + << tTest << " r " << rTest << " b " << bTest << ": " + << finalLogLikelihood + << (finalLogLikelihood < bestLogLikelihood ? " BEST" : ""); + if (bestT == 0 || finalLogLikelihood < bestLogLikelihood) + bestLogLikelihood = finalLogLikelihood, + bestT = tTest, bestR = rTest, bestB = bTest; + } + t = bestT, r = bestR, b = bestB; + LOG(RPiAwb, Debug) + << "Fine search found t " << t << " r " << r << " b " << b; +} + +void AwbBayes::awbBayes() +{ + /* + * May as well divide out G to save computeDelta2Sum from doing it over + * and over. + */ + for (auto &z : zones_) + z.R = z.R / (z.G + 1), z.B = z.B / (z.G + 1); + /* + * Get the current prior, and scale according to how many zones are + * valid... not entirely sure about this. + */ + ipa::Pwl prior = interpolatePrior(); + prior *= zones_.size() / (double)(statistics_->awbRegions.numRegions()); + prior.map([](double x, double y) { + LOG(RPiAwb, Debug) << "(" << x << "," << y << ")"; + }); + double t = coarseSearch(prior); + double r = config_.ctR.eval(t); + double b = config_.ctB.eval(t); + LOG(RPiAwb, Debug) + << "After coarse search: r " << r << " b " << b << " (gains r " + << 1 / r << " b " << 1 / b << ")"; + /* + * Not entirely sure how to handle the fine search yet. Mostly the + * estimated CT is already good enough, but the fine search allows us to + * wander transverely off the CT curve. Under some illuminants, where + * there may be more or less green light, this may prove beneficial, + * though I probably need more real datasets before deciding exactly how + * this should be controlled and tuned. + */ + fineSearch(t, r, b, prior); + LOG(RPiAwb, Debug) + << "After fine search: r " << r << " b " << b << " (gains r " + << 1 / r << " b " << 1 / b << ")"; + /* + * Write results out for the main thread to pick up. Remember to adjust + * the gains from the ones that the "canonical sensor" would require to + * the ones needed by *this* sensor. + */ + asyncResults_.temperatureK = t; + asyncResults_.gainR = 1.0 / r * config_.sensitivityR; + asyncResults_.gainG = 1.0; + asyncResults_.gainB = 1.0 / b * config_.sensitivityB; +} + +void AwbBayes::doAwb() +{ + prepareStats(); + LOG(RPiAwb, Debug) << "Valid zones: " << zones_.size(); + if (zones_.size() > bayesConfig_.minRegions) { + if (bayesConfig_.bayes) + awbBayes(); + else + awbGrey(); + LOG(RPiAwb, Debug) + << "CT found is " + << asyncResults_.temperatureK + << " with gains r " << asyncResults_.gainR + << " and b " << asyncResults_.gainB; + } + /* + * we're done with these; we may as well relinquish our hold on the + * pointer. + */ + statistics_.reset(); +} + +/* Register algorithm with the system. */ +static Algorithm *create(Controller *controller) +{ + return (Algorithm *)new AwbBayes(controller); +} +static RegisterAlgorithm reg(NAME, &create); + +} /* namespace RPiController */