From 45798ff89b89ed59ae39468fd552354e95e2fd87 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Fri, 1 Aug 2025 13:44:48 +0100 Subject: [PATCH 1/4] ipa: rpi: controller: awb: Separate Bayesian Awb into AwbBayes 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 dde4ac127..73c93dca3 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 365b595ff..de5fa59bf 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 2fb912541..8b2d8d1d4 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 @@ class Awb : public AwbAlgorithm 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 @@ class Awb : public AwbAlgorithm } }; -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 @@ class Awb : public AwbAlgorithm 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 @@ class Awb : public AwbAlgorithm 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 000000000..09233cec8 --- /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 */ From 7e16bd3790c8b929cc96c988f482d7e3a24f0004 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Fri, 1 Aug 2025 13:48:29 +0100 Subject: [PATCH 2/4] ipa: rpi: controller: awb: Add Neural Network Awb Add an Awb algorithm which uses neural networks. Signed-off-by: Peter Bailey --- src/ipa/rpi/controller/meson.build | 9 + src/ipa/rpi/controller/rpi/awb_nn.cpp | 442 ++++++++++++++++++++++++++ 2 files changed, 451 insertions(+) create mode 100644 src/ipa/rpi/controller/rpi/awb_nn.cpp diff --git a/src/ipa/rpi/controller/meson.build b/src/ipa/rpi/controller/meson.build index 73c93dca3..2541d073c 100644 --- a/src/ipa/rpi/controller/meson.build +++ b/src/ipa/rpi/controller/meson.build @@ -32,6 +32,15 @@ rpi_ipa_controller_deps = [ libcamera_private, ] +tflite_dep = dependency('tensorflow-lite', required : false) + +if tflite_dep.found() + rpi_ipa_controller_sources += files([ + 'rpi/awb_nn.cpp', + ]) + rpi_ipa_controller_deps += tflite_dep +endif + rpi_ipa_controller_lib = static_library('rpi_ipa_controller', rpi_ipa_controller_sources, include_directories : libipa_includes, dependencies : rpi_ipa_controller_deps) diff --git a/src/ipa/rpi/controller/rpi/awb_nn.cpp b/src/ipa/rpi/controller/rpi/awb_nn.cpp new file mode 100644 index 000000000..c309ca3f6 --- /dev/null +++ b/src/ipa/rpi/controller/rpi/awb_nn.cpp @@ -0,0 +1,442 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* + * Copyright (C) 2025, Raspberry Pi Ltd + * + * AWB control algorithm using neural network + */ + +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include "../awb_algorithm.h" +#include "../awb_status.h" +#include "../lux_status.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.nn.awb" + +namespace RPiController { + +struct AwbNNConfig { + AwbNNConfig() {} + int read(const libcamera::YamlObject ¶ms, AwbConfig &config); + + /* An empty model will check default locations for model.tflite */ + std::string model; + float minTemp; + float maxTemp; + + bool enableNn; + + /* CCM matrix for 5000K temperature */ + double ccm[9]; +}; + +class AwbNN : public Awb +{ +public: + AwbNN(Controller *controller = NULL); + ~AwbNN(); + char const *name() const override; + void initialise() override; + int read(const libcamera::YamlObject ¶ms) override; + +protected: + void doAwb() override; + void prepareStats() override; + +private: + bool isAutoEnabled() const; + AwbNNConfig nnConfig_; + void transverseSearch(double t, double &r, double &b); + RGB processZone(RGB zone, float red_gain, float blue_gain); + void awbNN(); + void loadModel(); + + libcamera::Size zoneSize_; + std::unique_ptr model_; + std::unique_ptr interpreter_; +}; + +int AwbNNConfig::read(const libcamera::YamlObject ¶ms, AwbConfig &config) +{ + model = params["model"].get(""); + minTemp = params["min_temp"].get(2800.0); + maxTemp = params["max_temp"].get(7600.0); + + for (int i = 0; i < 9; i++) + ccm[i] = params["ccm"][i].get(0.0); + + enableNn = params["enable_nn"].get(1); + + if (enableNn) { + if (!config.hasCtCurve()) { + LOG(RPiAwb, Error) << "CT curve not specified"; + enableNn = false; + } + + if (!model.empty() && model.find(".tflite") == std::string::npos) { + LOG(RPiAwb, Error) << "Model must be a .tflite file"; + enableNn = false; + } + + bool validCcm = true; + for (int i = 0; i < 9; i++) + if (ccm[i] == 0.0) + validCcm = false; + + if (!validCcm) { + LOG(RPiAwb, Error) << "CCM not specified or invalid"; + enableNn = false; + } + + if (!enableNn) { + LOG(RPiAwb, Warning) << "Neural Network AWB mis-configured - switch to Grey method"; + } + } + + if (!enableNn) { + config.sensitivityR = config.sensitivityB = 1.0; + config.greyWorld = true; + } + + return 0; +} + +AwbNN::AwbNN(Controller *controller) + : Awb(controller) +{ + zoneSize_ = getHardwareConfig().awbRegions; +} + +AwbNN::~AwbNN() +{ +} + +char const *AwbNN::name() const +{ + return NAME; +} + +int AwbNN::read(const libcamera::YamlObject ¶ms) +{ + int ret; + + ret = config_.read(params); + if (ret) + return ret; + + ret = nnConfig_.read(params, config_); + if (ret) + return ret; + + return 0; +} + +static bool checkTensorShape(TfLiteTensor *tensor, const int *expectedDims, const int expectedDimsSize) +{ + if (tensor->dims->size != expectedDimsSize) { + return false; + } + + for (int i = 0; i < tensor->dims->size; i++) { + if (tensor->dims->data[i] != expectedDims[i]) { + return false; + } + } + return true; +} + +static std::string buildDimString(const int *dims, const int dimsSize) +{ + std::string s = "["; + for (int i = 0; i < dimsSize; i++) { + s += std::to_string(dims[i]); + if (i < dimsSize - 1) + s += ","; + else + s += "]"; + } + return s; +} + +void AwbNN::loadModel() +{ + std::string modelPath; + if (getTarget() == "bcm2835") { + modelPath = "/ipa/rpi/vc4/awb_model.tflite"; + } else { + modelPath = "/ipa/rpi/pisp/awb_model.tflite"; + } + + if (nnConfig_.model.empty()) { + std::string root = utils::libcameraSourcePath(); + if (!root.empty()) { + modelPath = root + modelPath; + } else { + modelPath = LIBCAMERA_DATA_DIR + modelPath; + } + + if (!File::exists(modelPath)) { + LOG(RPiAwb, Error) << "No model file found in standard locations"; + nnConfig_.enableNn = false; + return; + } + } else { + modelPath = nnConfig_.model; + } + + LOG(RPiAwb, Debug) << "Attempting to load model from: " << modelPath; + + model_ = tflite::FlatBufferModel::BuildFromFile(modelPath.c_str()); + + if (!model_) { + LOG(RPiAwb, Error) << "Failed to load model from " << modelPath; + nnConfig_.enableNn = false; + return; + } + + tflite::MutableOpResolver resolver; + tflite::ops::builtin::BuiltinOpResolver builtin_resolver; + resolver.AddAll(builtin_resolver); + tflite::InterpreterBuilder(*model_, resolver)(&interpreter_); + if (!interpreter_) { + LOG(RPiAwb, Error) << "Failed to build interpreter for model " << nnConfig_.model; + nnConfig_.enableNn = false; + return; + } + + interpreter_->AllocateTensors(); + TfLiteTensor *inputTensor = interpreter_->input_tensor(0); + TfLiteTensor *inputLuxTensor = interpreter_->input_tensor(1); + TfLiteTensor *outputTensor = interpreter_->output_tensor(0); + if (!inputTensor || !inputLuxTensor || !outputTensor) { + LOG(RPiAwb, Error) << "Model missing input or output tensor"; + nnConfig_.enableNn = false; + return; + } + + const int expectedInputDims[] = { 1, (int)zoneSize_.height, (int)zoneSize_.width, 3 }; + const int expectedInputLuxDims[] = { 1 }; + const int expectedOutputDims[] = { 1 }; + + if (!checkTensorShape(inputTensor, expectedInputDims, 4)) { + LOG(RPiAwb, Error) << "Model input tensor dimension mismatch. Expected: " << buildDimString(expectedInputDims, 4) + << ", Got: " << buildDimString(inputTensor->dims->data, inputTensor->dims->size); + nnConfig_.enableNn = false; + return; + } + + if (!checkTensorShape(inputLuxTensor, expectedInputLuxDims, 1)) { + LOG(RPiAwb, Error) << "Model input lux tensor dimension mismatch. Expected: " << buildDimString(expectedInputLuxDims, 1) + << ", Got: " << buildDimString(inputLuxTensor->dims->data, inputLuxTensor->dims->size); + nnConfig_.enableNn = false; + return; + } + + if (!checkTensorShape(outputTensor, expectedOutputDims, 1)) { + LOG(RPiAwb, Error) << "Model output tensor dimension mismatch. Expected: " << buildDimString(expectedOutputDims, 1) + << ", Got: " << buildDimString(outputTensor->dims->data, outputTensor->dims->size); + nnConfig_.enableNn = false; + return; + } + + if (inputTensor->type != kTfLiteFloat32 || inputLuxTensor->type != kTfLiteFloat32 || outputTensor->type != kTfLiteFloat32) { + LOG(RPiAwb, Error) << "Model input and output tensors must be float32"; + nnConfig_.enableNn = false; + return; + } + + LOG(RPiAwb, Info) << "Model loaded successfully from " << modelPath; + LOG(RPiAwb, Debug) << "Model validation successful - Input Image: " + << buildDimString(expectedInputDims, 4) + << ", Input Lux: " << buildDimString(expectedInputLuxDims, 1) + << ", Output: " << buildDimString(expectedOutputDims, 1) << " floats"; +} + +void AwbNN::initialise() +{ + Awb::initialise(); + + if (nnConfig_.enableNn) { + loadModel(); + if (!nnConfig_.enableNn) { + LOG(RPiAwb, Warning) << "Neural Network AWB failed to load - switch to Grey method"; + config_.greyWorld = true; + config_.sensitivityR = config_.sensitivityB = 1.0; + } + } +} + +void AwbNN::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. + */ + generateStats(zones_, statistics_, 0.0, 0.0, getGlobalMetadata(), 0.0, 0.0, 0.0); + /* + * apply sensitivities, so values appear to come from our "canonical" + * sensor. + */ + for (auto &zone : zones_) { + zone.R *= config_.sensitivityR; + zone.B *= config_.sensitivityB; + } +} + +void AwbNN::transverseSearch(double t, double &r, double &b) +{ + int spanR = -1, spanB = -1; + config_.ctR.eval(t, &spanR); + config_.ctB.eval(t, &spanB); + + const int diff = 10; + double rDiff = config_.ctR.eval(t + diff, &spanR) - + config_.ctR.eval(t - diff, &spanR); + double bDiff = config_.ctB.eval(t + diff, &spanB) - + config_.ctB.eval(t - diff, &spanB); + + ipa::Pwl::Point transverse({ bDiff, -rDiff }); + if (transverse.length2() < 1e-6) + return; + + transverse = transverse / transverse.length(); + double transverseRange = config_.transverseNeg + config_.transversePos; + const int maxNumDeltas = 12; + int numDeltas = floor(transverseRange * 100 + 0.5) + 1; + numDeltas = numDeltas < 3 ? 3 : (numDeltas > maxNumDeltas ? maxNumDeltas : numDeltas); + + ipa::Pwl::Point points[maxNumDeltas]; + int bestPoint = 0; + + for (int i = 0; i < numDeltas; i++) { + points[i][0] = -config_.transverseNeg + + (transverseRange * i) / (numDeltas - 1); + ipa::Pwl::Point rbTest = ipa::Pwl::Point({ r, b }) + + transverse * points[i].x(); + double rTest = rbTest.x(), bTest = rbTest.y(); + double gainR = 1 / rTest, gainB = 1 / bTest; + double delta2Sum = computeDelta2Sum(gainR, gainB, 0.0, 0.0); + points[i][1] = delta2Sum; + if (points[i].y() < points[bestPoint].y()) + bestPoint = i; + } + + bestPoint = std::clamp(bestPoint, 1, numDeltas - 2); + ipa::Pwl::Point rbBest = ipa::Pwl::Point({ r, b }) + + transverse * interpolateQuadatric(points[bestPoint - 1], + points[bestPoint], + points[bestPoint + 1]); + double rBest = rbBest.x(), bBest = rbBest.y(); + + r = rBest, b = bBest; +} + +AwbNN::RGB AwbNN::processZone(AwbNN::RGB zone, float redGain, float blueGain) +{ + /* + * Renders the pixel at 5000K temperature + */ + RGB zoneGains = zone; + + zoneGains.R *= redGain; + zoneGains.G *= 1.0; + zoneGains.B *= blueGain; + + RGB zoneCcm; + + zoneCcm.R = nnConfig_.ccm[0] * zoneGains.R + nnConfig_.ccm[1] * zoneGains.G + nnConfig_.ccm[2] * zoneGains.B; + zoneCcm.G = nnConfig_.ccm[3] * zoneGains.R + nnConfig_.ccm[4] * zoneGains.G + nnConfig_.ccm[5] * zoneGains.B; + zoneCcm.B = nnConfig_.ccm[6] * zoneGains.R + nnConfig_.ccm[7] * zoneGains.G + nnConfig_.ccm[8] * zoneGains.B; + + return zoneCcm; +} + +void AwbNN::awbNN() +{ + float *inputData = interpreter_->typed_input_tensor(0); + float *inputLux = interpreter_->typed_input_tensor(1); + + float redGain = 1.0 / config_.ctR.eval(5000); + float blueGain = 1.0 / config_.ctB.eval(5000); + + for (uint i = 0; i < zoneSize_.height; i++) { + for (uint j = 0; j < zoneSize_.width; j++) { + uint zoneIdx = i * zoneSize_.width + j; + + RGB processedZone = processZone(zones_[zoneIdx] * (1.0 / 65535), redGain, blueGain); + uint baseIdx = zoneIdx * 3; + + inputData[baseIdx + 0] = static_cast(processedZone.R); + inputData[baseIdx + 1] = static_cast(processedZone.G); + inputData[baseIdx + 2] = static_cast(processedZone.B); + } + } + + inputLux[0] = static_cast(lux_); + + TfLiteStatus status = interpreter_->Invoke(); + if (status != kTfLiteOk) { + LOG(RPiAwb, Error) << "Model inference failed with status: " << status; + return; + } + + float *outputData = interpreter_->typed_output_tensor(0); + + double t = outputData[0]; + + LOG(RPiAwb, Debug) << "Model output temperature: " << t; + + t = std::clamp(t, mode_->ctLo, mode_->ctHi); + + double r = config_.ctR.eval(t); + double b = config_.ctB.eval(t); + + transverseSearch(t, r, b); + + LOG(RPiAwb, Debug) << "After transverse search: Temperature: " << t << " Red gain: " << 1.0 / r << " Blue gain: " << 1.0 / b; + + asyncResults_.temperatureK = t; + asyncResults_.gainR = 1.0 / r * config_.sensitivityR; + asyncResults_.gainG = 1.0; + asyncResults_.gainB = 1.0 / b * config_.sensitivityB; +} + +void AwbNN::doAwb() +{ + prepareStats(); + if (zones_.size() == (zoneSize_.width * zoneSize_.height) && nnConfig_.enableNn) { + awbNN(); + } else { + awbGrey(); + } + statistics_.reset(); +} + +/* Register algorithm with the system. */ +static Algorithm *create(Controller *controller) +{ + return (Algorithm *)new AwbNN(controller); +} +static RegisterAlgorithm reg(NAME, &create); + +} /* namespace RPiController */ From 87868c464ec174bddfaf84a0646959a7557975a5 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Fri, 1 Aug 2025 13:52:49 +0100 Subject: [PATCH 3/4] ipa: rpi: pisp: vc4: Update tuning files for new awb and add model Update the tuning files to include the new Awb algorithm. It is enabled by renaming disable.rpi.nn.awb to rpi.nn.awb and rpi.awb to disable.rpi.awb. Add a model for the Awb algorithm to use by default. Signed-off-by: Peter Bailey --- src/ipa/rpi/pisp/data/awb_model.tflite | Bin 0 -> 47624 bytes src/ipa/rpi/pisp/data/imx219.json | 65 ++++++++++++++- src/ipa/rpi/pisp/data/imx296.json | 64 ++++++++++++++- src/ipa/rpi/pisp/data/imx296_16mm.json | 64 ++++++++++++++- src/ipa/rpi/pisp/data/imx296_6mm.json | 64 ++++++++++++++- src/ipa/rpi/pisp/data/imx477.json | 63 +++++++++++++++ src/ipa/rpi/pisp/data/imx477_16mm.json | 65 ++++++++++++++- src/ipa/rpi/pisp/data/imx477_6mm.json | 65 ++++++++++++++- src/ipa/rpi/pisp/data/imx477_scientific.json | 79 ++++++++++++++++++- src/ipa/rpi/pisp/data/imx500.json | 67 ++++++++++++++++ src/ipa/rpi/pisp/data/imx708.json | 64 ++++++++++++++- src/ipa/rpi/pisp/data/imx708_wide.json | 62 +++++++++++++++ src/ipa/rpi/pisp/data/meson.build | 7 ++ src/ipa/rpi/pisp/data/ov5647.json | 63 +++++++++++++++ src/ipa/rpi/vc4/data/awb_model.tflite | Bin 0 -> 42968 bytes src/ipa/rpi/vc4/data/imx219.json | 64 +++++++++++++++ src/ipa/rpi/vc4/data/imx296.json | 64 +++++++++++++++ src/ipa/rpi/vc4/data/imx477.json | 69 ++++++++++++++++ src/ipa/rpi/vc4/data/imx500.json | 67 ++++++++++++++++ src/ipa/rpi/vc4/data/imx708.json | 72 +++++++++++++++++ src/ipa/rpi/vc4/data/imx708_wide.json | 62 +++++++++++++++ src/ipa/rpi/vc4/data/meson.build | 8 ++ src/ipa/rpi/vc4/data/ov5647.json | 64 +++++++++++++++ 23 files changed, 1254 insertions(+), 8 deletions(-) create mode 100644 src/ipa/rpi/pisp/data/awb_model.tflite create mode 100644 src/ipa/rpi/vc4/data/awb_model.tflite diff --git a/src/ipa/rpi/pisp/data/awb_model.tflite b/src/ipa/rpi/pisp/data/awb_model.tflite new file mode 100644 index 0000000000000000000000000000000000000000..2c3774e6f3a7974cd4714db39fdb8c935a98416c GIT binary patch literal 47624 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�u?M**=%V~QwdcfJ!Yle+uaXwr=e!w3g`@J3O3jT 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 literal 0 HcmV?d00001 diff --git a/src/ipa/rpi/vc4/data/imx219.json b/src/ipa/rpi/vc4/data/imx219.json index c43d6db4c..51f4e2e0f 100644 --- a/src/ipa/rpi/vc4/data/imx219.json +++ b/src/ipa/rpi/vc4/data/imx219.json @@ -128,6 +128,70 @@ "transverse_neg": 0.04881 } }, + { + "disable.rpi.nn.awb": + { + "modes": + { + "auto": + { + "lo": 2500, + "hi": 8000 + }, + "incandescent": + { + "lo": 2500, + "hi": 3000 + }, + "tungsten": + { + "lo": 3000, + "hi": 3500 + }, + "fluorescent": + { + "lo": 4000, + "hi": 4700 + }, + "indoor": + { + "lo": 3000, + "hi": 5000 + }, + "daylight": + { + "lo": 5500, + "hi": 6500 + }, + "cloudy": + { + "lo": 7000, + "hi": 8600 + } + }, + "ct_curve": + [ + 2498.0, 0.9309, 0.3599, + 2911.0, 0.8682, 0.4283, + 2919.0, 0.8358, 0.4621, + 3627.0, 0.7646, 0.5327, + 4600.0, 0.6079, 0.6721, + 5716.0, 0.5712, 0.7017, + 8575.0, 0.4331, 0.8037 + ], + "sensitivity_r": 1.05, + "sensitivity_b": 1.05, + "transverse_pos": 0.04791, + "transverse_neg": 0.04881, + "ccm": + [ + 2.2229345364238413, -0.7596721523178808, -0.46326238410596027, + -0.6834893874172185, 2.7118816887417223, -1.02839940397351, + -0.2613746357615894, -0.668015927152318, 1.9293905629139072 + ], + "enable_nn": 1 + } + }, { "rpi.agc": { diff --git a/src/ipa/rpi/vc4/data/imx296.json b/src/ipa/rpi/vc4/data/imx296.json index c9b9ee618..0472b8cfd 100644 --- a/src/ipa/rpi/vc4/data/imx296.json +++ b/src/ipa/rpi/vc4/data/imx296.json @@ -128,6 +128,70 @@ "transverse_neg": 0.02374 } }, + { + "disable.rpi.nn.awb": + { + "modes": + { + "auto": + { + "lo": 2500, + "hi": 7600 + }, + "incandescent": + { + "lo": 2500, + "hi": 3000 + }, + "tungsten": + { + "lo": 3000, + "hi": 3500 + }, + "fluorescent": + { + "lo": 4000, + "hi": 4700 + }, + "indoor": + { + "lo": 3000, + "hi": 5000 + }, + "daylight": + { + "lo": 5500, + "hi": 6500 + }, + "cloudy": + { + "lo": 7000, + "hi": 7600 + } + }, + "ct_curve": + [ + 2500.0, 0.5386, 0.2458, + 2800.0, 0.4883, 0.3303, + 2900.0, 0.4855, 0.3349, + 3620.0, 0.4203, 0.4367, + 4560.0, 0.3455, 0.5444, + 5600.0, 0.2948, 0.6124, + 7400.0, 0.2336, 0.6894 + ], + "sensitivity_r": 1.05, + "sensitivity_b": 1.05, + "transverse_pos": 0.03093, + "transverse_neg": 0.02374, + "ccm": + [ + 2.1073753846153847, -0.8054946153846154, -0.30188076923076923, + -0.43306999999999995, 2.162828076923077, -0.7297680769230768, + -0.126655, -0.5027626923076922, 1.6294176923076922 + ], + "enable_nn": 1 + } + }, { "rpi.agc": { diff --git a/src/ipa/rpi/vc4/data/imx477.json b/src/ipa/rpi/vc4/data/imx477.json index 46f512876..4b40299cf 100644 --- a/src/ipa/rpi/vc4/data/imx477.json +++ b/src/ipa/rpi/vc4/data/imx477.json @@ -133,6 +133,75 @@ "transverse_neg": 0.04429 } }, + { + "disable.rpi.nn.awb": + { + "modes": + { + "auto": + { + "lo": 2500, + "hi": 8000 + }, + "incandescent": + { + "lo": 2500, + "hi": 3000 + }, + "tungsten": + { + "lo": 3000, + "hi": 3500 + }, + "fluorescent": + { + "lo": 4000, + "hi": 4700 + }, + "indoor": + { + "lo": 3000, + "hi": 5000 + }, + "daylight": + { + "lo": 5500, + "hi": 6500 + }, + "cloudy": + { + "lo": 7000, + "hi": 8600 + } + }, + "ct_curve": + [ + 2360.0, 0.6009, 0.3093, + 2848.0, 0.5071, 0.4, + 2903.0, 0.4905, 0.4392, + 3628.0, 0.4261, 0.5564, + 3643.0, 0.4228, 0.5623, + 4660.0, 0.3529, 0.68, + 5579.0, 0.3227, 0.7, + 6125.0, 0.3129, 0.71, + 6671.0, 0.3065, 0.72, + 7217.0, 0.3014, 0.73, + 7763.0, 0.295, 0.74, + 9505.0, 0.2524, 0.7856 + ], + "sensitivity_r": 1.05, + "sensitivity_b": 1.05, + "transverse_pos": 0.0238, + "transverse_neg": 0.04429, + "ccm": + [ + 2.1643743343419066, -0.972589984871407, -0.19177768532526474, + -0.3769567095310136, 2.0993768608169443, -0.722416815431165, + -0.11786965204236007, -0.4893621633888049, 1.607231815431165 + ], + "enable_nn": 1 + } + }, { "rpi.agc": { diff --git a/src/ipa/rpi/vc4/data/imx500.json b/src/ipa/rpi/vc4/data/imx500.json index 224ffb92a..e12068a53 100644 --- a/src/ipa/rpi/vc4/data/imx500.json +++ b/src/ipa/rpi/vc4/data/imx500.json @@ -131,6 +131,73 @@ "transverse_neg": 0.02626 } }, + { + "disable.rpi.nn.awb": + { + "modes": + { + "auto": + { + "lo": 2800, + "hi": 8000 + }, + "incandescent": + { + "lo": 2800, + "hi": 3000 + }, + "tungsten": + { + "lo": 3000, + "hi": 3500 + }, + "fluorescent": + { + "lo": 4000, + "hi": 4700 + }, + "indoor": + { + "lo": 3000, + "hi": 5000 + }, + "daylight": + { + "lo": 5500, + "hi": 6500 + }, + "cloudy": + { + "lo": 7000, + "hi": 7600 + } + }, + "ct_curve": + [ + 2800.0, 0.7126, 0.3567, + 2860.0, 0.6681, 0.4042, + 2880.0, 0.6651, 0.4074, + 3580.0, 0.5674, 0.5091, + 3650.0, 0.5629, 0.5137, + 4500.0, 0.4792, 0.5982, + 4570.0, 0.4752, 0.6022, + 5648.0, 0.4137, 0.6628, + 5717.0, 0.4116, 0.6648, + 7600.0, 0.3609, 0.7138 + ], + "sensitivity_r": 1.0, + "sensitivity_b": 1.0, + "transverse_pos": 0.02798, + "transverse_neg": 0.02626, + "ccm": + [ + 1.6856933395176252, -0.4760917810760668, -0.20960155844155848, + -0.3666382560296846, 1.9130496103896104, -0.5464153432282004, + -0.060413803339517624, -0.4878164935064935, 1.5482282745825604 + ], + "enable_nn": 1 + } + }, { "rpi.agc": { diff --git a/src/ipa/rpi/vc4/data/imx708.json b/src/ipa/rpi/vc4/data/imx708.json index 5aae842ef..56271cbcd 100644 --- a/src/ipa/rpi/vc4/data/imx708.json +++ b/src/ipa/rpi/vc4/data/imx708.json @@ -136,6 +136,78 @@ "transverse_neg": 0.03061 } }, + { + "disable.rpi.nn.awb": + { + "modes": + { + "auto": + { + "lo": 2500, + "hi": 8000 + }, + "incandescent": + { + "lo": 2500, + "hi": 3000 + }, + "tungsten": + { + "lo": 3000, + "hi": 3500 + }, + "fluorescent": + { + "lo": 4000, + "hi": 4700 + }, + "indoor": + { + "lo": 3000, + "hi": 5000 + }, + "daylight": + { + "lo": 5500, + "hi": 6500 + }, + "cloudy": + { + "lo": 7000, + "hi": 8600 + } + }, + "ct_curve": + [ + 2498.0, 0.8733, 0.2606, + 2821.0, 0.7707, 0.3245, + 2925.0, 0.7338, 0.3499, + 2926.0, 0.7193, 0.3603, + 2951.0, 0.7144, 0.3639, + 2954.0, 0.7111, 0.3663, + 3578.0, 0.6038, 0.4516, + 3717.0, 0.5861, 0.4669, + 3784.0, 0.5786, 0.4737, + 4485.0, 0.5113, 0.5368, + 4615.0, 0.4994, 0.5486, + 4671.0, 0.4927, 0.5554, + 5753.0, 0.4274, 0.6246, + 5773.0, 0.4265, 0.6256, + 7433.0, 0.3723, 0.6881 + ], + "sensitivity_r": 1.05, + "sensitivity_b": 1.05, + "transverse_pos": 0.03148, + "transverse_neg": 0.03061, + "ccm": + [ + 1.5407949606299214, -0.3714970078740158, -0.16929511811023623, + -0.2801589763779528, 1.649028503937008, -0.36886236220472446, + 0.004032519685039371, -0.5251851181102363, 1.521162598425197 + ], + "enable_nn": 1 + } + }, { "rpi.agc": { diff --git a/src/ipa/rpi/vc4/data/imx708_wide.json b/src/ipa/rpi/vc4/data/imx708_wide.json index a678dc328..684550f0a 100644 --- a/src/ipa/rpi/vc4/data/imx708_wide.json +++ b/src/ipa/rpi/vc4/data/imx708_wide.json @@ -126,6 +126,68 @@ "transverse_neg": 0.01601 } }, + { + "disable.rpi.nn.awb": + { + "modes": + { + "auto": + { + "lo": 2500, + "hi": 8000 + }, + "incandescent": + { + "lo": 2500, + "hi": 3000 + }, + "tungsten": + { + "lo": 3000, + "hi": 3500 + }, + "fluorescent": + { + "lo": 4000, + "hi": 4700 + }, + "indoor": + { + "lo": 3000, + "hi": 5000 + }, + "daylight": + { + "lo": 5500, + "hi": 6500 + }, + "cloudy": + { + "lo": 7000, + "hi": 8600 + } + }, + "ct_curve": + [ + 2750.0, 0.7881, 0.2849, + 2940.0, 0.7559, 0.3103, + 3650.0, 0.6291, 0.4206, + 4625.0, 0.5336, 0.5161, + 5715.0, 0.4668, 0.5898 + ], + "sensitivity_r": 1.05, + "sensitivity_b": 1.05, + "transverse_pos": 0.01165, + "transverse_neg": 0.01601, + "ccm": + [ + 1.5820866588602653, -0.39406808743169397, -0.1880145042935207, + -0.3101711553473849, 1.756938087431694, -0.44677099921935987, + -0.018062732240437158, -0.5139293442622951, 1.5319991100702577 + ], + "enable_nn": 1 + } + }, { "rpi.agc": { diff --git a/src/ipa/rpi/vc4/data/meson.build b/src/ipa/rpi/vc4/data/meson.build index b42f5f6c8..7516c653b 100644 --- a/src/ipa/rpi/vc4/data/meson.build +++ b/src/ipa/rpi/vc4/data/meson.build @@ -29,6 +29,14 @@ conf_files = files([ 'uncalibrated.json', ]) +model_files = files([ + 'awb_model.tflite' +]) + + install_data(conf_files, install_dir : ipa_data_dir / 'rpi' / 'vc4', install_tag : 'runtime') + +install_data(model_files, + install_dir : ipa_data_dir / 'rpi' / 'vc4') diff --git a/src/ipa/rpi/vc4/data/ov5647.json b/src/ipa/rpi/vc4/data/ov5647.json index 38d4d2656..56e33b5b8 100644 --- a/src/ipa/rpi/vc4/data/ov5647.json +++ b/src/ipa/rpi/vc4/data/ov5647.json @@ -128,6 +128,70 @@ "transverse_neg": 0.04313 } }, + { + "disable.rpi.nn.awb": + { + "modes": + { + "auto": + { + "lo": 2500, + "hi": 8000 + }, + "incandescent": + { + "lo": 2500, + "hi": 3000 + }, + "tungsten": + { + "lo": 3000, + "hi": 3500 + }, + "fluorescent": + { + "lo": 4000, + "hi": 4700 + }, + "indoor": + { + "lo": 3000, + "hi": 5000 + }, + "daylight": + { + "lo": 5500, + "hi": 6500 + }, + "cloudy": + { + "lo": 7000, + "hi": 8600 + } + }, + "ct_curve": + [ + 2500.0, 1.0289, 0.4503, + 2803.0, 0.9428, 0.5108, + 2914.0, 0.9406, 0.5127, + 3605.0, 0.8261, 0.6249, + 4540.0, 0.7331, 0.7533, + 5699.0, 0.6715, 0.8627, + 8625.0, 0.6081, 1.0012 + ], + "sensitivity_r": 1.05, + "sensitivity_b": 1.05, + "transverse_pos": 0.0321, + "transverse_neg": 0.04313, + "ccm": + [ + 2.041588151260504, -0.5494553781512606, -0.49214025210084034, + -0.5116488235294118, 1.9901442857142857, -0.47849546218487393, + -0.10519773109243696, -0.641700168067227, 1.7468953781512604 + ], + "enable_nn": 1 + } + }, { "rpi.agc": { From a68be7e699a12b016dd4b51203f86c061eee3cb6 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Fri, 29 Aug 2025 09:50:31 +0100 Subject: [PATCH 4/4] ipa: rpi: controller: Ignore algorithms starting with disable Prevent an algorithm starting with "disable" from being loaded. Signed-off-by: Peter Bailey --- src/ipa/rpi/controller/controller.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ipa/rpi/controller/controller.cpp b/src/ipa/rpi/controller/controller.cpp index df45dcd34..5eee0693d 100644 --- a/src/ipa/rpi/controller/controller.cpp +++ b/src/ipa/rpi/controller/controller.cpp @@ -145,6 +145,12 @@ int Controller::read(char const *filename) int Controller::createAlgorithm(const std::string &name, const YamlObject ¶ms) { + if (name.find("disable") == 0) { + LOG(RPiController, Debug) + << "Algorithm \"" << name << "\" is disabled"; + return 0; + } + auto it = getAlgorithms().find(name); if (it == getAlgorithms().end()) { LOG(RPiController, Warning)