From patchwork Tue Jan 27 17:13:16 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: David Plowman X-Patchwork-Id: 25979 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 B49B9C3220 for ; Tue, 27 Jan 2026 17:15:17 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 6229E61FD8; Tue, 27 Jan 2026 18:15:17 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=raspberrypi.com header.i=@raspberrypi.com header.b="LoMO570e"; dkim-atps=neutral Received: from mail-wm1-x329.google.com (mail-wm1-x329.google.com [IPv6:2a00:1450:4864:20::329]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 5EE9361FD1 for ; Tue, 27 Jan 2026 18:15:13 +0100 (CET) Received: by mail-wm1-x329.google.com with SMTP id 5b1f17b1804b1-47ee301a06aso67799075e9.0 for ; Tue, 27 Jan 2026 09:15:13 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=raspberrypi.com; s=google; t=1769534112; x=1770138912; 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=gIq6iIB1QP0iS7LyASPjJNmChvgxVeOVwcswcwW+Mq4=; b=LoMO570e6ZitoZ2DDFGzFxiV6jEvbTYoT5cOJmZOPljfAdFeAsy3F21ZKdEBaFLXy9 LeTo9v85KeRqvLgXOvZv0dwvNmQAMsUHPoOa8TGrqgvKMrTvi8YEUQr7rkRs4yByUNYh IqcNDFINH5c2PSwgMtgHR+9nU6POPABZynm/iO+JUBNXDkxjaAFki0YBDd/RuP5P0Tcl UA4l0RA5Ux0YqxWPlBU5K2mN7/WsVWEA4na3M6/9JRD9UKk8itZHLc9LLJw1EjlSSfU5 zDAyEVqkaomp96GFe6ZHIvH/YKgXC38Y1wejdVm95pNI8eW29VfAEuTcZsJ5ldyfGU6i n1lQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1769534112; x=1770138912; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=gIq6iIB1QP0iS7LyASPjJNmChvgxVeOVwcswcwW+Mq4=; b=BDrwDHEouqAFdWQPtBNxfvckq+WCLmy4uASMv+Uf/R24zBnCyzsGlWGlkZpUS18Tn0 Y8pLb5OMnY4ExrYGWnjTMaPKyaYMWBhR5iT57erVghYQDauLWPgZtwoMZ/RG2FO2OQV7 KGhO1v5n0AzhWasyvEQJLFypIBqZLmZSv8VZOvwJ+n+HU+r96kkLb+M5HfEAVJ66ZA+N NB1+YQ5slAOuW9Mnj+e3hPrCdqkGV3zmUfkXiCB732iq8XZQKi9Mg9ZA3x3Zbjd9CmFV EOUYpYe0g/BvaC2uPSBV/V/koQeSWJj7t17ZbiRocdXJkAG/JJBv5ufsiOxgANLZgF2x w5SQ== X-Gm-Message-State: AOJu0YxMSj+5dc8fhKpQ5tOy/0dXxGMB2UTgI238x3bOGS0lHteUAhRL KlrqekCj9BNkuAePZwDd+UZ6jFG/xRStkZKQ4mJ2KtXUy10sBVDqbqPJVafaaOpIG1LQppnd2LC M9UZD X-Gm-Gg: AZuq6aJ5sKn2PeGuR0y2sCQxg+kwc4rpinoXb6FNtnuv75t7bNL+jPJ9/9VB/eLawWE b/BsIlwLvwhF1JoWaJdAn0IAWS8y+0FvhhDQoGHOImIyJ7F/ugKT51re/ZeCD6nKK5UIVuaoBr9 0yHHEXBaR3LizfMAEADO+IW4otpNaR2pCmSUgxW1r2xn2CP/j8iLnOs4waNRv0lLPenrK5ub8Qq On7WVDS4I4PJokGwTDL0k3kk8Prc5dPQNRBrQevhdRD+SRjP4qLZNI8pZlV1QCoSq2sx0MEWeKe K6pEh/YBRfyiA5bC+Ct4q50m9/vZ8GVKb3zYpN4S7DTn5nIvd6rO7xalQIJ10QvAcbnc9sy/p7z /BQ9ehLjh/Ys66pmrVEAwMIdS+GCkcx3Xz/wMO1FF3bnfH7qBFNTlLKf1g3zr0cNxN0/JOG/d4p NOtk8gmWUr7hWF+7UHtAiV7aV9HwkqAXk9e6goQGJANr2JL03/D3ljQPrYuAkChi3P1NkojP948 HqndU07Lgn/woqqGqiJF7n3kD5frQ== X-Received: by 2002:a05:600c:6096:b0:477:9b4a:a82 with SMTP id 5b1f17b1804b1-48069c75255mr37573585e9.35.1769534111437; Tue, 27 Jan 2026 09:15:11 -0800 (PST) Received: from davidp-pi5.pitowers.org ([2a00:1098:3142:1f:88ea:c658:5b20:5e46]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-4806cddffe9sm12267865e9.4.2026.01.27.09.15.10 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 27 Jan 2026 09:15:10 -0800 (PST) From: David Plowman To: libcamera-devel@lists.libcamera.org Cc: Peter Bailey , David Plowman , Naushir Patuck Subject: [PATCH v6 1/4] ipa: rpi: controller: awb: Separate Bayesian AWB into AwbBayes Date: Tue, 27 Jan 2026 17:13:16 +0000 Message-ID: <20260127171506.17480-2-david.plowman@raspberrypi.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260127171506.17480-1-david.plowman@raspberrypi.com> References: <20260127171506.17480-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 Reviewed-by: David Plowman Reviewed-by: Naushir Patuck --- src/ipa/rpi/controller/meson.build | 1 + src/ipa/rpi/controller/rpi/awb.cpp | 409 +++------------------ src/ipa/rpi/controller/rpi/awb.h | 101 +++--- src/ipa/rpi/controller/rpi/awb_bayes.cpp | 444 +++++++++++++++++++++++ 4 files changed, 534 insertions(+), 421 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 c13c4853..c8637906 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..5ee0717b 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,24 +59,16 @@ 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 { public: - Awb(Controller *controller = NULL); + Awb(Controller *controller = nullptr); ~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..7aaac20d --- /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 new AwbBayes(controller); +} +static RegisterAlgorithm reg(NAME, &create); + +} /* namespace RPiController */ From patchwork Tue Jan 27 17:13:17 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: David Plowman X-Patchwork-Id: 25980 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 261F7C328D for ; Tue, 27 Jan 2026 17:15:20 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 85A9F61FD6; Tue, 27 Jan 2026 18:15:18 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=raspberrypi.com header.i=@raspberrypi.com header.b="OucjrZTA"; dkim-atps=neutral Received: from mail-wm1-x32d.google.com (mail-wm1-x32d.google.com [IPv6:2a00:1450:4864:20::32d]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 88E3361FD2 for ; Tue, 27 Jan 2026 18:15:13 +0100 (CET) Received: by mail-wm1-x32d.google.com with SMTP id 5b1f17b1804b1-47ee3a63300so67258345e9.2 for ; Tue, 27 Jan 2026 09:15:13 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=raspberrypi.com; s=google; t=1769534113; x=1770138913; 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=x2bQHuSs/sNthSP3D+231dZk6wtL/itXkk2+f/mLY6w=; b=OucjrZTAuZQ4PeWU4bpoutZ4PvnxZ09ov1mutzkKJqxgOMfJUsF3gYiFTbn1QAa/pS Ag77Jfnkrz+1kDmJWnXU9yorZzb9BycNQwCS1falwDJ1PFiq6H7GCt7JOAOQGxhMj5Hi 0Rr72HG9DCrE8xxVYuelq+mIhzNaVUFsNoye87jN/uVSbQ4Q5EABEkWkzV/DUhuIdiIn H+3Qr8eTgrpFYnZ017qRVFpm1D39OwpKCjEanbAiv0AsBIDMdksoWxbZ1h4QemqWd3uX fp+1c3xbbckZCHXSYRNkbX/YQrL510kKt9+DU0lo8afpHgso/JSGjKUNR+xtr2UNKxs8 9V+Q== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1769534113; x=1770138913; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=x2bQHuSs/sNthSP3D+231dZk6wtL/itXkk2+f/mLY6w=; b=chBxjjUmJASro1frzsZcrQ879TEShlBDC4pv9vUyAd/gUeDKVci0hQwr01akSnq4bV cG2JC2FR/OdokzxsQuOOpd7TBncVAwZLmtktpRL+5GVqWfWVzuSeNRFPt0QWGIDQcW76 X2hLDa5PG7KP86Qp2s97k+926Bmws2C/TCjrMMDZlUQjH7/StuUUQvA97OEkumsVaW46 ZgCbwgziwYdSTYg/hY+HvukyQqI40QnS3spoWhC3fAsykKGOiULI9ydy+y6tM3jZtvUH 8mnIufgGg0jHLPRmwf3jf8Ev7hpHkDBQXAKyjgjkDAbYOpJGlt79M9OW1xumayh3vrsp zZGw== X-Gm-Message-State: AOJu0YwnsawdEIqyuwkCEfltvG7OmKBIUFWXtIWl5doH+6x9UfuuhnIs jGlNf/B0MM9y6TY7rj4QL6dbyl2vf5w11w1uKmINk0/T5pYWVi70Ypb7UnEsmlzzNDOsgrd5CvZ HtCRd X-Gm-Gg: AZuq6aLlMjMzSRmst6hIXTm1Nxdb+o2kCwcuDOLEqqGp1S9gJZLjQostmnROUlOTFS3 6Mwr3ljE4JqcNQbXFr1EWR3OGPRy7RWI48nYC7vdWZPEaJTvuumf4iKWE5fd1UK7gQrpBYDoNrr 37Cy24FbsntOqUjGXVQfreMT2s9izLm1rQxk2+pYv5NbmWeceGSCqXQCpQdgHcDzbKHNh8Pq3wS JPGuPFqSGkwqTeuom6WvZ7RUmAms5F6G5a8g1Nb9JeoYrv7r9vO7lD4ytbB9LB8eQjQVbcBnqyq xPjcCwfyY72468BMKu3oNpINRAjauVKZf4xKG0h5OMiDgglMGRCtSAvx0cmLobe8We+pcdvoZit +v9vrUNTwjnQBK9keh0vFFhSvC6xdGHfyqBAxrtYoGzrCFvZyLqURBSCKClMldgE1wzSeYvmLSb pBUFx1GM5b18EpVEYsr4z52cFROJFaLknnfY0Fc2q40w0DaVAPPPN3lvM/e9eaELR1S/Uj67/3F iynb7O1O5qXiVhpwSdtC2kcvblvhQ== X-Received: by 2002:a05:600c:4e54:b0:46e:6d5f:f68 with SMTP id 5b1f17b1804b1-4806c7cfb14mr13159595e9.12.1769534112411; Tue, 27 Jan 2026 09:15:12 -0800 (PST) Received: from davidp-pi5.pitowers.org ([2a00:1098:3142:1f:88ea:c658:5b20:5e46]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-4806cddffe9sm12267865e9.4.2026.01.27.09.15.11 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 27 Jan 2026 09:15:11 -0800 (PST) From: David Plowman To: libcamera-devel@lists.libcamera.org Cc: Peter Bailey , David Plowman , Naushir Patuck Subject: [PATCH v6 2/4] ipa: rpi: controller: awb: Add Neural Network AWB Date: Tue, 27 Jan 2026 17:13:17 +0000 Message-ID: <20260127171506.17480-3-david.plowman@raspberrypi.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260127171506.17480-1-david.plowman@raspberrypi.com> References: <20260127171506.17480-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 Add an AWB algorithm which uses neural networks. Signed-off-by: Peter Bailey Reviewed-by: David Plowman Reviewed-by: Naushir Patuck --- meson_options.txt | 5 + src/ipa/rpi/controller/meson.build | 9 + src/ipa/rpi/controller/rpi/awb_nn.cpp | 437 ++++++++++++++++++++++++++ 3 files changed, 451 insertions(+) create mode 100644 src/ipa/rpi/controller/rpi/awb_nn.cpp diff --git a/meson_options.txt b/meson_options.txt index c052e85a..07847294 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -76,6 +76,11 @@ option('qcam', value : 'auto', description : 'Compile the qcam test application') +option('rpi-awb-nn', + type : 'feature', + value : 'auto', + description : 'Enable the Raspberry Pi Neural Network AWB algorithm') + option('test', type : 'boolean', value : false, diff --git a/src/ipa/rpi/controller/meson.build b/src/ipa/rpi/controller/meson.build index c8637906..03ee7c20 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 : get_option('rpi-awb-nn')) + +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 00000000..1c205126 --- /dev/null +++ b/src/ipa/rpi/controller/rpi/awb_nn.cpp @@ -0,0 +1,437 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* + * Copyright (C) 2025, Raspberry Pi Ltd + * + * AWB control algorithm using neural network + * + * The AWB Neural Network algorithm can be run entirely with the code here + * and the suppllied TFLite models. Those interested in the full model + * definitions, or who may want to re-train the models should visit + * + * https://github.com/raspberrypi/awb_nn + * + * where you will find full source code for the models, the full datasets + * used for training our supplied models, and full instructions for capturing + * your own images and re-training the models for your own use cases. + */ + +#include + +#include +#include + +#include +#include +#include + +#include "../awb_algorithm.h" +#include "../awb_status.h" +#include "../lux_status.h" +#include "libipa/pwl.h" + +#include "awb.h" + +using namespace libcamera; + +LOG_DECLARE_CATEGORY(RPiAwb) + +constexpr double kDefaultCT = 4500.0; + +/* + * The neural networks are trained to work on images rendered at a canonical + * colour temperature. That value is 5000K, which must be reproduced here. + */ +constexpr double kNetworkCanonicalCT = 5000.0; + +#define NAME "rpi.nn.awb" + +namespace RPiController { + +struct AwbNNConfig { + AwbNNConfig() = default; + 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 canonical network CT */ + double ccm[9]; +}; + +class AwbNN : public Awb +{ +public: + AwbNN(Controller *controller = nullptr); + ~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 misconfigured - 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) +{ + return std::equal(expectedDims, expectedDims + expectedDimsSize, + tensor->dims->data, tensor->dims->data + tensor->dims->size); +} + +static std::string buildDimString(const int *dims, const int dimsSize) +{ + return "[" + utils::join(Span(dims, dimsSize), ",") + "]"; +} + +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 = std::clamp(numDeltas, 3, maxNumDeltas); + + 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]); + r = rbBest.x(); + b = rbBest.y(); +} + +AwbNN::RGB AwbNN::processZone(AwbNN::RGB zone, float redGain, float blueGain) +{ + /* + * Renders the pixel at canonical network colour 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(kNetworkCanonicalCT); + float blueGain = 1.0 / config_.ctB.eval(kNetworkCanonicalCT); + + for (unsigned int i = 0; i < zoneSize_.height; i++) { + for (unsigned int j = 0; j < zoneSize_.width; j++) { + unsigned int zoneIdx = i * zoneSize_.width + j; + + RGB processedZone = processZone(zones_[zoneIdx] * (1.0 / 65535), redGain, blueGain); + unsigned int 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 new AwbNN(controller); +} +static RegisterAlgorithm reg(NAME, &create); + +} /* namespace RPiController */ From patchwork Tue Jan 27 17:13:18 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: David Plowman X-Patchwork-Id: 25981 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 D28E5C32E7 for ; Tue, 27 Jan 2026 17:15:21 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 103BA61FD5; Tue, 27 Jan 2026 18:15:20 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=raspberrypi.com header.i=@raspberrypi.com header.b="nqmr3FwO"; dkim-atps=neutral Received: from mail-wm1-x32c.google.com (mail-wm1-x32c.google.com [IPv6:2a00:1450:4864:20::32c]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id C130361FBF for ; Tue, 27 Jan 2026 18:15:14 +0100 (CET) Received: by mail-wm1-x32c.google.com with SMTP id 5b1f17b1804b1-4801d98cf39so45292845e9.1 for ; Tue, 27 Jan 2026 09:15:14 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=raspberrypi.com; s=google; t=1769534114; x=1770138914; 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=rHmpk8rHk/IlsYU76ryoqPKQ/4NYcVsL5LSqEDs/vLU=; b=nqmr3FwOVkPhcyNo5qpum0sO/G4IolVFW5jdfZQhJEu3BB0ZISJXdyo3zQZr3LNvjK 35yD3Epua2hf7OzTG+WmPlOEvoDoH/q8EDyQL2t6HcBgEov1ec1kKJAFa4j9OAAYk5ta 04SbCuIhuHw002iV5CNLUdhENcrM112wBOiKGgWYeZkQQ8Y4Ynv2C0ubJO+7UmOV1t9r 2jlfWurtdX9Tp9xmsNWLTOqsKc2z/ZzGxzSKjTQ6bs/XyK8NUYCtf43W1D8QV0wfaUgE BGX8rGdG9jG9uu8eVg+2PZcTa81cwy+ONjlVgLYQLiz7MjgciRHa4gtkflyQopivMFsB +Pcg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1769534114; x=1770138914; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=rHmpk8rHk/IlsYU76ryoqPKQ/4NYcVsL5LSqEDs/vLU=; b=FXK+EY/atcc2+auHXUM5citE4asv6dtWO5O394dGLIdgCRZvAMCWCjrIp3jsTJ0zwc /uWRHE2NKaSiM/YGCyCAmXAm8UuJ7gAtmkhDJBJstyFNPpXNgVamHM1EshwczLIuWZlT ql30im6zFrG5niKYblnUNln0cDZMNvUCf2fDjUUFN+nwLceSMr3Vvyjvjd1yWIkERUS/ IqRcflNs4AoH1yGerX0Nt0D3EYQPp8IZ+G3t0WReDnzYar5hvTxzsg0oxMhgcbYq5rJq h0mGYOHsbDUKenZmaLaAy+GdXF/6nnptjyTRiQ+I0vFbVCjobVRP/lIaI0apmn1grhYS 4THA== X-Gm-Message-State: AOJu0YxvckKUkwsvV2vvVLHdcM5ZdyzWU+Ds0IT6FQxSXsyUd1WnK8/+ qPuXMYYNcY9PFLDStyzyGz6JpIBclHwQTEL/TEFCvTUIntxmcUa6srkQx2Cn/PXTiCdNZIW0oAZ ys3P3 X-Gm-Gg: AZuq6aJgNvwxGh+3Zl2Il4YKYjlPsYpIfljjC8bODzGTn3j4Wcu1b3bcz5CGkLeQwQC r0KB31OissSpJdvSWlmt1lf7vRghK7xabdnD5h4EaxrGvaX3OBv8QR4xlDdQktYPlpnvsDB2eCL Yhyr82iBPrg+knYsMp1XTAegpFWSlOs6DbWcb1hN+s8TlOA28Fc2/BWkHuXUUo1ictnmxK8VfBt 7Tu4oinaxNeFJy6kUpJ4iXYW92J/niB51lg8pet9Uuxbx3Q8iL+3VXC2L+XNl+9l5e2i0uGF5wh 3WBgbLKVEyVtDSc/fDvvNgL+wZYmuIcn90muHSfw0iwd4yUcO6HO5x6PaHoXHkKr0AgZcQCa4rt NWwWXgxrVlQgBuWoiYYRYnqbMj2I5PlCuIJjCnTaAN/gQWLWKWDKglBazaktIfN2/tyak9w9heF kJqMUtm4rsfuPXTZeuHKf9TDfNNrgRDWpsAe8zgwYYN3m/Q6QsXrmfZeRK8LJlMCVJjhmq7lXPW kjKYEkEwyIPMfwZcMPW17wkGPw82A== X-Received: by 2002:a05:600c:8b16:b0:480:1a9a:e571 with SMTP id 5b1f17b1804b1-48069c8c109mr28174895e9.22.1769534113730; Tue, 27 Jan 2026 09:15:13 -0800 (PST) Received: from davidp-pi5.pitowers.org ([2a00:1098:3142:1f:88ea:c658:5b20:5e46]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-4806cddffe9sm12267865e9.4.2026.01.27.09.15.12 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 27 Jan 2026 09:15:12 -0800 (PST) From: David Plowman To: libcamera-devel@lists.libcamera.org Cc: Peter Bailey , David Plowman , Naushir Patuck Subject: [PATCH v6 3/4] ipa: rpi: controller: Ignore algorithms that are not enabled Date: Tue, 27 Jan 2026 17:13:18 +0000 Message-ID: <20260127171506.17480-4-david.plowman@raspberrypi.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260127171506.17480-1-david.plowman@raspberrypi.com> References: <20260127171506.17480-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 Algorithms may now contain an "enabled" field which can be set to false to disable it and prevent it from being loaded. If not present, algorithms are treated as enabled by default for backwards compatability. We additionally prevent duplicate versions of the same algorithm type (such as AWB) from loading, and flag an error. This will prevent undefined behaviour if (for example) two distinct AWB algorithms are trying to run simultaneously. Signed-off-by: Peter Bailey Reviewed-by: David Plowman Reviewed-by: Naushir Patuck --- src/ipa/rpi/controller/controller.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/ipa/rpi/controller/controller.cpp b/src/ipa/rpi/controller/controller.cpp index df45dcd3..88de6f36 100644 --- a/src/ipa/rpi/controller/controller.cpp +++ b/src/ipa/rpi/controller/controller.cpp @@ -145,6 +145,14 @@ int Controller::read(char const *filename) int Controller::createAlgorithm(const std::string &name, const YamlObject ¶ms) { + /* Any algorithm may be disabled by setting "enabled" to false. */ + bool enabled = params["enabled"].get(true); + LOG(RPiController, Debug) + << "Algorithm " << name << ": " + << (enabled ? "enabled" : "disabled"); + if (!enabled) + return 0; + auto it = getAlgorithms().find(name); if (it == getAlgorithms().end()) { LOG(RPiController, Warning) @@ -152,6 +160,16 @@ int Controller::createAlgorithm(const std::string &name, const YamlObject ¶m return 0; } + /* Do not allow duplicate versions of algorithms (e.g. AWB) to run. */ + size_t pos = name.find_last_of('.'); + std::string const &algoType = + pos == std::string::npos ? name : name.substr(pos + 1); + if (getAlgorithm(algoType)) { + LOG(RPiController, Error) + << "Algorithm type '" << algoType << "' already exists"; + return -1; + } + Algorithm *algo = (*it->second)(this); int ret = algo->read(params); if (ret) From patchwork Tue Jan 27 17:13:19 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: David Plowman X-Patchwork-Id: 25982 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 1FB00C3220 for ; Tue, 27 Jan 2026 17:15:23 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 9D88861FD4; Tue, 27 Jan 2026 18:15:21 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=raspberrypi.com header.i=@raspberrypi.com header.b="iren3EEw"; dkim-atps=neutral Received: from mail-wm1-x332.google.com (mail-wm1-x332.google.com [IPv6:2a00:1450:4864:20::332]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 98FAB61FC4 for ; Tue, 27 Jan 2026 18:15:15 +0100 (CET) Received: by mail-wm1-x332.google.com with SMTP id 5b1f17b1804b1-4806cc07ce7so2152805e9.1 for ; Tue, 27 Jan 2026 09:15:15 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=raspberrypi.com; s=google; t=1769534115; x=1770138915; 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=T2+HzXeseFMVP5kcIsd4loJqi8DLlU0W/k4qV42qctI=; b=iren3EEwlNUASrpdlDXrAk9jn8BwG46JVAD0DvQl+X6WgqDZAZC7V77dvvAUdw3rSF 4uoMKItrFrYWl/HljZYZ7QlMJ0iBq59Xwimub3DT4lNeRtLAf+kW3HwzLcK1T21+uYks rIQ8N3QfWeSPzXhq3X0TMZBN2WaAIG/zKAeHOfLSfPuPnbYPqFZJ7hsi2D2XrIwRsj/s FvUsumIqO6ygNYjhcOJiibOTjWsBrtalljfDEyDhhohw8B71R6ccvcLNnoPpagUothLG 4DI9qv+1xbIlTuSxBsYa7dK3jyCflPKllK1ivWT199aYYOPkxQyzV6Ej634uer+5DUkX OZ7A== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1769534115; x=1770138915; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=T2+HzXeseFMVP5kcIsd4loJqi8DLlU0W/k4qV42qctI=; b=PxM4klaUg6iONNlAq5/iRB+QWOsXxHFGMNRHAz7MqN3RaSm7xJwoAbDWKeig1hzvRw A3MXOaqPSnZO/kuXxT1R8a6C5g/El++yh8+ll8GzqTU2YxWwt1vN6LAQOQ9cEGb2hK9O MuQZ2N4bJgNdg0vu/xvmxfuPo9CCcnSKlmASpumZZh2EKlcKInno8vcxrc8Vn+tMLgvM DuzS95TQ+RCLl3BT9gtoI1ktzu7GgdPdfBD62vn0q9gL5jbiQzKePFa6RcPk1PjYIYPG vRJo9w//F7cqaZe35jsoW1CqsjPq/CA3T/BEn2d38YjEYC9p3c4ojPGdB/TndJDKhs6j xM7Q== X-Gm-Message-State: AOJu0YzNb3Hmzd0Yr80Vor7glyF8O/F8Bnek5nt+Va6q4JeRFNjoHxpR W/bEZb3v/iCZTWUuy2zpj85rTT+vvGJWfg/BKqiyJvoBcw0PsG5TYwQ56Z/jD8MPruqHkmVisMA nAAWi X-Gm-Gg: AZuq6aIiWukwvUttrKNBOkDVGIFSPo9C5aX2nDQQd/clv1PGiNfbd3ne2u8juhk0YRh UFYiganvQa1olK8P+kvTrjYABg8Wr4mxRvysu+lTZAZLsue2+pX6lTuTYZ9IPpc78xZD1jrODs8 1KvQa+FgCxmtwcZuh8Ejsux4f2/yFatzf/rY37wYeGso0/3E75lqwhJCkHirel9csnJ9i2peJDW q4oUz++oSBULjNExGbMD2nibG3WcVb/za9bj1ZeHoQGTM8fVKRO6Ze2b8pdU9woW/vHqo++C1wb 6TAIDHcC+POESxHsAyS96pSL7yq0D9n0wcIyEHJaZONBkJ2FGINSoTj8d5c07obh1A5UzPus0Tb GrRjDcQeUZbJ116TQ5ZVIlOLRH6zckHc0lYx08vW63HDT8VLnxf0CRwUYgBDHvpOIFEo4oCMhZ7 76K52FGQxRRusSuYK0AC2otZJcPbh5aZUO2TXwlkar2tbMwzBj/iGg6/aznb2pMwJWkIz9S0fAf 4ptxu6XpAtpOcLJCQIy/nsa9zVaTA== X-Received: by 2002:a05:600c:138d:b0:480:1e92:dc65 with SMTP id 5b1f17b1804b1-48069c9462emr36931305e9.31.1769534114562; Tue, 27 Jan 2026 09:15:14 -0800 (PST) Received: from davidp-pi5.pitowers.org ([2a00:1098:3142:1f:88ea:c658:5b20:5e46]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-4806cddffe9sm12267865e9.4.2026.01.27.09.15.13 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 27 Jan 2026 09:15:14 -0800 (PST) From: David Plowman To: libcamera-devel@lists.libcamera.org Cc: Peter Bailey , David Plowman , Naushir Patuck Subject: [PATCH v6 4/4] ipa: rpi: pisp: vc4: Update tuning files for new AWB Date: Tue, 27 Jan 2026 17:13:19 +0000 Message-ID: <20260127171506.17480-5-david.plowman@raspberrypi.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260127171506.17480-1-david.plowman@raspberrypi.com> References: <20260127171506.17480-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 Update the tuning files to include the new AWB algorithm. It is enabled by setting "enabled" to true for the AWB algorithm that you want, and the same field to false for the one you don't want. Note that you may enable only one of the two algorithms! The AWB models themselves are not included with libcamera. They will be supplied from the Raspberry Pi software repositories. Signed-off-by: Peter Bailey Reviewed-by: David Plowman Reviewed-by: Naushir Patuck --- 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 | 65 ++++++++++++++++ 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/imx708.json | 64 ++++++++++++++++ src/ipa/rpi/pisp/data/imx708_wide.json | 64 ++++++++++++++++ src/ipa/rpi/pisp/data/ov5647.json | 65 ++++++++++++++++ src/ipa/rpi/vc4/data/imx219.json | 66 ++++++++++++++++ src/ipa/rpi/vc4/data/imx296.json | 66 ++++++++++++++++ src/ipa/rpi/vc4/data/imx477.json | 71 ++++++++++++++++++ src/ipa/rpi/vc4/data/imx708.json | 74 ++++++++++++++++++ src/ipa/rpi/vc4/data/imx708_wide.json | 64 ++++++++++++++++ src/ipa/rpi/vc4/data/ov5647.json | 66 ++++++++++++++++ 17 files changed, 1131 insertions(+) diff --git a/src/ipa/rpi/pisp/data/imx219.json b/src/ipa/rpi/pisp/data/imx219.json index 727f8e2d..7aea190a 100644 --- a/src/ipa/rpi/pisp/data/imx219.json +++ b/src/ipa/rpi/pisp/data/imx219.json @@ -109,6 +109,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -195,6 +196,70 @@ "transverse_neg": 0.034 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "modes": + { + "auto": + { + "lo": 2500, + "hi": 7700 + }, + "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": 8000 + } + }, + "ct_curve": + [ + 2860.0, 0.9514, 0.4156, + 2960.0, 0.9289, 0.4372, + 3603.0, 0.8305, 0.5251, + 4650.0, 0.6756, 0.6433, + 5858.0, 0.6193, 0.6807, + 7580.0, 0.5019, 0.7495 + ], + "sensitivity_r": 1.0, + "sensitivity_b": 1.0, + "transverse_pos": 0.03392, + "transverse_neg": 0.034, + "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/pisp/data/imx296.json b/src/ipa/rpi/pisp/data/imx296.json index d833c837..f03f875e 100644 --- a/src/ipa/rpi/pisp/data/imx296.json +++ b/src/ipa/rpi/pisp/data/imx296.json @@ -109,6 +109,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -194,6 +195,69 @@ "transverse_neg": 0.02154 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "modes": + { + "auto": + { + "lo": 2500, + "hi": 7700 + }, + "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": 8000 + } + }, + "ct_curve": + [ + 2875.0, 0.4699, 0.3209, + 3610.0, 0.4089, 0.4265, + 4640.0, 0.3281, 0.5417, + 5912.0, 0.2992, 0.5771, + 7630.0, 0.2285, 0.6524 + ], + "sensitivity_r": 1.0, + "sensitivity_b": 1.0, + "transverse_pos": 0.01783, + "transverse_neg": 0.02154, + "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/pisp/data/imx296_16mm.json b/src/ipa/rpi/pisp/data/imx296_16mm.json index 62344533..91f0a379 100644 --- a/src/ipa/rpi/pisp/data/imx296_16mm.json +++ b/src/ipa/rpi/pisp/data/imx296_16mm.json @@ -109,6 +109,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -194,6 +195,69 @@ "transverse_neg": 0.02154 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "modes": + { + "auto": + { + "lo": 2500, + "hi": 7700 + }, + "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": 8000 + } + }, + "ct_curve": + [ + 2875.0, 0.4699, 0.3209, + 3610.0, 0.4089, 0.4265, + 4640.0, 0.3281, 0.5417, + 5912.0, 0.2992, 0.5771, + 7630.0, 0.2285, 0.6524 + ], + "sensitivity_r": 1.0, + "sensitivity_b": 1.0, + "transverse_pos": 0.01783, + "transverse_neg": 0.02154, + "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/pisp/data/imx296_6mm.json b/src/ipa/rpi/pisp/data/imx296_6mm.json index c71a0327..30c4a2ad 100644 --- a/src/ipa/rpi/pisp/data/imx296_6mm.json +++ b/src/ipa/rpi/pisp/data/imx296_6mm.json @@ -109,6 +109,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -194,6 +195,69 @@ "transverse_neg": 0.02154 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "modes": + { + "auto": + { + "lo": 2500, + "hi": 7700 + }, + "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": 8000 + } + }, + "ct_curve": + [ + 2875.0, 0.4699, 0.3209, + 3610.0, 0.4089, 0.4265, + 4640.0, 0.3281, 0.5417, + 5912.0, 0.2992, 0.5771, + 7630.0, 0.2285, 0.6524 + ], + "sensitivity_r": 1.0, + "sensitivity_b": 1.0, + "transverse_pos": 0.01783, + "transverse_neg": 0.02154, + "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/pisp/data/imx477.json b/src/ipa/rpi/pisp/data/imx477.json index 1dce5964..af006e32 100644 --- a/src/ipa/rpi/pisp/data/imx477.json +++ b/src/ipa/rpi/pisp/data/imx477.json @@ -109,6 +109,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -195,6 +196,70 @@ "transverse_neg": 0.02255 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "modes": + { + "auto": + { + "lo": 2500, + "hi": 7700 + }, + "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": 8000 + } + }, + "ct_curve": + [ + 2850.0, 0.4307, 0.3957, + 2960.0, 0.4159, 0.4313, + 3580.0, 0.3771, 0.5176, + 4559.0, 0.3031, 0.6573, + 5881.0, 0.2809, 0.6942, + 7600.0, 0.2263, 0.7762 + ], + "sensitivity_r": 1.0, + "sensitivity_b": 1.0, + "transverse_pos": 0.02634, + "transverse_neg": 0.02255, + "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/pisp/data/imx477_16mm.json b/src/ipa/rpi/pisp/data/imx477_16mm.json index 3c835341..a290b879 100644 --- a/src/ipa/rpi/pisp/data/imx477_16mm.json +++ b/src/ipa/rpi/pisp/data/imx477_16mm.json @@ -109,6 +109,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -195,6 +196,70 @@ "transverse_neg": 0.02255 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "modes": + { + "auto": + { + "lo": 2500, + "hi": 7700 + }, + "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": 8000 + } + }, + "ct_curve": + [ + 2850.0, 0.4307, 0.3957, + 2960.0, 0.4159, 0.4313, + 3580.0, 0.3771, 0.5176, + 4559.0, 0.3031, 0.6573, + 5881.0, 0.2809, 0.6942, + 7600.0, 0.2263, 0.7762 + ], + "sensitivity_r": 1.0, + "sensitivity_b": 1.0, + "transverse_pos": 0.02634, + "transverse_neg": 0.02255, + "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/pisp/data/imx477_6mm.json b/src/ipa/rpi/pisp/data/imx477_6mm.json index af4f7891..c70f3fb9 100644 --- a/src/ipa/rpi/pisp/data/imx477_6mm.json +++ b/src/ipa/rpi/pisp/data/imx477_6mm.json @@ -109,6 +109,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -195,6 +196,70 @@ "transverse_neg": 0.02255 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "modes": + { + "auto": + { + "lo": 2500, + "hi": 7700 + }, + "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": 8000 + } + }, + "ct_curve": + [ + 2850.0, 0.4307, 0.3957, + 2960.0, 0.4159, 0.4313, + 3580.0, 0.3771, 0.5176, + 4559.0, 0.3031, 0.6573, + 5881.0, 0.2809, 0.6942, + 7600.0, 0.2263, 0.7762 + ], + "sensitivity_r": 1.0, + "sensitivity_b": 1.0, + "transverse_pos": 0.02634, + "transverse_neg": 0.02255, + "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/pisp/data/imx477_scientific.json b/src/ipa/rpi/pisp/data/imx477_scientific.json index 1d9bae9b..cdca9f43 100644 --- a/src/ipa/rpi/pisp/data/imx477_scientific.json +++ b/src/ipa/rpi/pisp/data/imx477_scientific.json @@ -64,6 +64,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -164,6 +165,84 @@ "coarse_step": 0.1 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "modes": + { + "auto": + { + "lo": 2500, + "hi": 7700 + }, + "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": 8000 + } + }, + "ct_curve": + [ + 2000.0, 0.6331025775790707, 0.27424225990946915, + 2200.0, 0.5696117366212947, 0.3116091368689487, + 2400.0, 0.5204264653110015, 0.34892179554105873, + 2600.0, 0.48148675531667223, 0.38565229719076793, + 2800.0, 0.450085403501908, 0.42145684622485047, + 3000.0, 0.42436130159169017, 0.45611835670028816, + 3200.0, 0.40300023695527337, 0.48950766215198593, + 3400.0, 0.3850520052612984, 0.5215567075837261, + 3600.0, 0.36981508088230314, 0.5522397906415475, + 4100.0, 0.333468007836758, 0.5909770465167908, + 4600.0, 0.31196097364221376, 0.6515706327327178, + 5100.0, 0.2961860409294588, 0.7068178946570284, + 5600.0, 0.2842607232745885, 0.7564837749584288, + 6100.0, 0.2750265787051251, 0.8006183524920533, + 6600.0, 0.2677057225584924, 0.8398879225373039, + 7100.0, 0.2617955199757274, 0.8746456080032436, + 7600.0, 0.25693714288250125, 0.905569559506562, + 8100.0, 0.25287531441063316, 0.9331696750390895, + 8600.0, 0.24946601483331993, 0.9576820904825795 + ], + "sensitivity_r": 1.05, + "sensitivity_b": 1.05, + "transverse_pos": 0.0238, + "transverse_neg": 0.04429, + "coarse_step": 0.1, + "ccm": + [ + 2.003815467921944, -1.0081613204143252, 0.005840157117467748, + -0.18090523909630973, 1.597736399205449, -0.4326323675585491, + 0.05055066369087284, -0.6057020512156361, 1.5577256973300102 + ], + "enable_nn": 1 + } + }, { "rpi.agc": { diff --git a/src/ipa/rpi/pisp/data/imx708.json b/src/ipa/rpi/pisp/data/imx708.json index d86afc2e..9c5c11dc 100644 --- a/src/ipa/rpi/pisp/data/imx708.json +++ b/src/ipa/rpi/pisp/data/imx708.json @@ -109,6 +109,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -194,6 +195,69 @@ "transverse_neg": 0.01831 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "modes": + { + "auto": + { + "lo": 2500, + "hi": 7700 + }, + "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": 8000 + } + }, + "ct_curve": + [ + 2964.0, 0.7451, 0.3213, + 3610.0, 0.6119, 0.4443, + 4640.0, 0.5168, 0.5419, + 5910.0, 0.4436, 0.6229, + 7590.0, 0.3847, 0.6921 + ], + "sensitivity_r": 1.0, + "sensitivity_b": 1.0, + "transverse_pos": 0.01752, + "transverse_neg": 0.01831, + "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/pisp/data/imx708_wide.json b/src/ipa/rpi/pisp/data/imx708_wide.json index 7208823a..b6eaa698 100644 --- a/src/ipa/rpi/pisp/data/imx708_wide.json +++ b/src/ipa/rpi/pisp/data/imx708_wide.json @@ -109,6 +109,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -194,6 +195,69 @@ "transverse_neg": 0.01376 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "modes": + { + "auto": + { + "lo": 2500, + "hi": 7700 + }, + "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": 8000 + } + }, + "ct_curve": + [ + 2868.0, 0.6419, 0.3613, + 3603.0, 0.5374, 0.4787, + 4620.0, 0.4482, 0.5813, + 5901.0, 0.3883, 0.6514, + 7610.0, 0.3279, 0.7232 + ], + "sensitivity_r": 1.0, + "sensitivity_b": 1.0, + "transverse_pos": 0.01908, + "transverse_neg": 0.01376, + "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/pisp/data/ov5647.json b/src/ipa/rpi/pisp/data/ov5647.json index d534af31..318b6536 100644 --- a/src/ipa/rpi/pisp/data/ov5647.json +++ b/src/ipa/rpi/pisp/data/ov5647.json @@ -109,6 +109,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -195,6 +196,70 @@ "transverse_neg": 0.03906 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "modes": + { + "auto": + { + "lo": 2500, + "hi": 7700 + }, + "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": 8000 + } + }, + "ct_curve": + [ + 2873.0, 1.0463, 0.5142, + 2965.0, 1.0233, 0.5284, + 3606.0, 0.8947, 0.6314, + 4700.0, 0.7665, 0.7897, + 5890.0, 0.7055, 0.8933, + 7600.0, 0.6482, 1.0119 + ], + "sensitivity_r": 1.0, + "sensitivity_b": 1.0, + "transverse_pos": 0.04072, + "transverse_neg": 0.03906, + "ccm": + [ + 2.041588151260504, -0.5494553781512606, -0.49214025210084034, + -0.5116488235294118, 1.9901442857142857, -0.47849546218487393, + -0.10519773109243696, -0.641700168067227, 1.7468953781512604 + ], + "enable_nn": 1 + } + }, { "rpi.agc": { diff --git a/src/ipa/rpi/vc4/data/imx219.json b/src/ipa/rpi/vc4/data/imx219.json index 8098889e..d40c38b1 100644 --- a/src/ipa/rpi/vc4/data/imx219.json +++ b/src/ipa/rpi/vc4/data/imx219.json @@ -41,6 +41,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -128,6 +129,71 @@ "transverse_neg": 0.04881 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "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 7c8589b2..edc35164 100644 --- a/src/ipa/rpi/vc4/data/imx296.json +++ b/src/ipa/rpi/vc4/data/imx296.json @@ -41,6 +41,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -128,6 +129,71 @@ "transverse_neg": 0.02374 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "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 7d922d68..99960a30 100644 --- a/src/ipa/rpi/vc4/data/imx477.json +++ b/src/ipa/rpi/vc4/data/imx477.json @@ -41,6 +41,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -133,6 +134,76 @@ "transverse_neg": 0.04429 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "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/imx708.json b/src/ipa/rpi/vc4/data/imx708.json index ec17243d..3ae24087 100644 --- a/src/ipa/rpi/vc4/data/imx708.json +++ b/src/ipa/rpi/vc4/data/imx708.json @@ -41,6 +41,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -136,6 +137,79 @@ "transverse_neg": 0.03061 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "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 e94f2441..dda58497 100644 --- a/src/ipa/rpi/vc4/data/imx708_wide.json +++ b/src/ipa/rpi/vc4/data/imx708_wide.json @@ -41,6 +41,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -126,6 +127,69 @@ "transverse_neg": 0.01601 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "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/ov5647.json b/src/ipa/rpi/vc4/data/ov5647.json index ca561e59..548be0a9 100644 --- a/src/ipa/rpi/vc4/data/ov5647.json +++ b/src/ipa/rpi/vc4/data/ov5647.json @@ -41,6 +41,7 @@ { "rpi.awb": { + "enabled": true, "priors": [ { "lux": 0, @@ -128,6 +129,71 @@ "transverse_neg": 0.04313 } }, + { + "rpi.nn.awb": + { + "enabled": false, + "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": {