[{"id":37312,"web_url":"https://patchwork.libcamera.org/comment/37312/","msgid":"<CAHW6GYJcjEuBCt+EGYF+H0dedHJ=b3dw62Bs0ygq5qcXh7SHYg@mail.gmail.com>","date":"2025-12-11T15:36:53","subject":"Re: [PATCH v2 2/4] ipa: rpi: controller: awb: Add Neural Network AWB","submitter":{"id":42,"url":"https://patchwork.libcamera.org/api/people/42/","name":"David Plowman","email":"david.plowman@raspberrypi.com"},"content":"Hi\n\nOn Thu, 11 Dec 2025 at 14:28, David Plowman\n<david.plowman@raspberrypi.com> wrote:\n>\n> From: Peter Bailey <peter.bailey@raspberrypi.com>\n>\n> Add an AWB algorithm which uses neural networks.\n>\n> Signed-off-by: Peter Bailey <peter.bailey@raspberrypi.com>\n> ---\n>  meson_options.txt                     |   5 +\n>  src/ipa/rpi/controller/meson.build    |   9 +\n>  src/ipa/rpi/controller/rpi/awb_nn.cpp | 446 ++++++++++++++++++++++++++\n>  3 files changed, 460 insertions(+)\n>  create mode 100644 src/ipa/rpi/controller/rpi/awb_nn.cpp\n>\n> diff --git a/meson_options.txt b/meson_options.txt\n> index 5954e028..89eece52 100644\n> --- a/meson_options.txt\n> +++ b/meson_options.txt\n> @@ -78,6 +78,11 @@ option('qcam',\n>          value : 'disabled',\n>          description : 'Compile the qcam test application')\n>\n> +option('rpi-awb-nn',\n> +        type : 'feature',\n> +        value : 'auto',\n> +        description : 'Enable the Raspberry Pi Neural Network AWB algorithm')\n> +\n>  option('test',\n>          type : 'boolean',\n>          value : false,\n> diff --git a/src/ipa/rpi/controller/meson.build b/src/ipa/rpi/controller/meson.build\n> index 90d9e285..9c261d99 100644\n> --- a/src/ipa/rpi/controller/meson.build\n> +++ b/src/ipa/rpi/controller/meson.build\n> @@ -33,6 +33,15 @@ rpi_ipa_controller_deps = [\n>      libcamera_private,\n>  ]\n>\n> +tflite_dep = dependency('tensorflow-lite', required : false)\n\nOops. I edited this to read\n\ntflite_dep = dependency('tensorflow-lite', required : get_option('rpi-awb-nn'))\n\nand then failed to hit the save button. Version 3 incoming shortly!\n\nDavid\n\n> +\n> +if tflite_dep.found()\n> +    rpi_ipa_controller_sources += files([\n> +        'rpi/awb_nn.cpp',\n> +    ])\n> +    rpi_ipa_controller_deps += tflite_dep\n> +endif\n> +\n>  rpi_ipa_controller_lib = static_library('rpi_ipa_controller', rpi_ipa_controller_sources,\n>                                          include_directories : libipa_includes,\n>                                          dependencies : rpi_ipa_controller_deps)\n> diff --git a/src/ipa/rpi/controller/rpi/awb_nn.cpp b/src/ipa/rpi/controller/rpi/awb_nn.cpp\n> new file mode 100644\n> index 00000000..35d1270e\n> --- /dev/null\n> +++ b/src/ipa/rpi/controller/rpi/awb_nn.cpp\n> @@ -0,0 +1,446 @@\n> +/* SPDX-License-Identifier: BSD-2-Clause */\n> +/*\n> + * Copyright (C) 2025, Raspberry Pi Ltd\n> + *\n> + * AWB control algorithm using neural network\n> + */\n> +\n> +#include <chrono>\n> +#include <condition_variable>\n> +#include <thread>\n> +\n> +#include <libcamera/base/file.h>\n> +#include <libcamera/base/log.h>\n> +\n> +#include <tensorflow/lite/interpreter.h>\n> +#include <tensorflow/lite/kernels/register.h>\n> +#include <tensorflow/lite/model.h>\n> +\n> +#include \"../awb_algorithm.h\"\n> +#include \"../awb_status.h\"\n> +#include \"../lux_status.h\"\n> +#include \"libipa/pwl.h\"\n> +\n> +#include \"alsc_status.h\"\n> +#include \"awb.h\"\n> +\n> +using namespace libcamera;\n> +\n> +LOG_DECLARE_CATEGORY(RPiAwb)\n> +\n> +constexpr double kDefaultCT = 4500.0;\n> +\n> +/*\n> + * The neural networks are trained to work on images rendered at a canonical\n> + * colour temperature. That value is 5000K, which must be reproduced here.\n> + */\n> +constexpr double kNetworkCanonicalCT = 5000.0;\n> +\n> +#define NAME \"rpi.nn.awb\"\n> +\n> +namespace RPiController {\n> +\n> +struct AwbNNConfig {\n> +       AwbNNConfig() {}\n> +       int read(const libcamera::YamlObject &params, AwbConfig &config);\n> +\n> +       /* An empty model will check default locations for model.tflite */\n> +       std::string model;\n> +       float minTemp;\n> +       float maxTemp;\n> +\n> +       bool enableNn;\n> +\n> +       /* CCM matrix for canonical network CT */\n> +       double ccm[9];\n> +};\n> +\n> +class AwbNN : public Awb\n> +{\n> +public:\n> +       AwbNN(Controller *controller = NULL);\n> +       ~AwbNN();\n> +       char const *name() const override;\n> +       void initialise() override;\n> +       int read(const libcamera::YamlObject &params) override;\n> +\n> +protected:\n> +       void doAwb() override;\n> +       void prepareStats() override;\n> +\n> +private:\n> +       bool isAutoEnabled() const;\n> +       AwbNNConfig nnConfig_;\n> +       void transverseSearch(double t, double &r, double &b);\n> +       RGB processZone(RGB zone, float red_gain, float blue_gain);\n> +       void awbNN();\n> +       void loadModel();\n> +\n> +       libcamera::Size zoneSize_;\n> +       std::unique_ptr<tflite::FlatBufferModel> model_;\n> +       std::unique_ptr<tflite::Interpreter> interpreter_;\n> +};\n> +\n> +int AwbNNConfig::read(const libcamera::YamlObject &params, AwbConfig &config)\n> +{\n> +       model = params[\"model\"].get<std::string>(\"\");\n> +       minTemp = params[\"min_temp\"].get<float>(2800.0);\n> +       maxTemp = params[\"max_temp\"].get<float>(7600.0);\n> +\n> +       for (int i = 0; i < 9; i++)\n> +               ccm[i] = params[\"ccm\"][i].get<double>(0.0);\n> +\n> +       enableNn = params[\"enable_nn\"].get<int>(1);\n> +\n> +       if (enableNn) {\n> +               if (!config.hasCtCurve()) {\n> +                       LOG(RPiAwb, Error) << \"CT curve not specified\";\n> +                       enableNn = false;\n> +               }\n> +\n> +               if (!model.empty() && model.find(\".tflite\") == std::string::npos) {\n> +                       LOG(RPiAwb, Error) << \"Model must be a .tflite file\";\n> +                       enableNn = false;\n> +               }\n> +\n> +               bool validCcm = true;\n> +               for (int i = 0; i < 9; i++)\n> +                       if (ccm[i] == 0.0)\n> +                               validCcm = false;\n> +\n> +               if (!validCcm) {\n> +                       LOG(RPiAwb, Error) << \"CCM not specified or invalid\";\n> +                       enableNn = false;\n> +               }\n> +\n> +               if (!enableNn) {\n> +                       LOG(RPiAwb, Warning) << \"Neural Network AWB mis-configured - switch to Grey method\";\n> +               }\n> +       }\n> +\n> +       if (!enableNn) {\n> +               config.sensitivityR = config.sensitivityB = 1.0;\n> +               config.greyWorld = true;\n> +       }\n> +\n> +       return 0;\n> +}\n> +\n> +AwbNN::AwbNN(Controller *controller)\n> +       : Awb(controller)\n> +{\n> +       zoneSize_ = getHardwareConfig().awbRegions;\n> +}\n> +\n> +AwbNN::~AwbNN()\n> +{\n> +}\n> +\n> +char const *AwbNN::name() const\n> +{\n> +       return NAME;\n> +}\n> +\n> +int AwbNN::read(const libcamera::YamlObject &params)\n> +{\n> +       int ret;\n> +\n> +       ret = config_.read(params);\n> +       if (ret)\n> +               return ret;\n> +\n> +       ret = nnConfig_.read(params, config_);\n> +       if (ret)\n> +               return ret;\n> +\n> +       return 0;\n> +}\n> +\n> +static bool checkTensorShape(TfLiteTensor *tensor, const int *expectedDims, const int expectedDimsSize)\n> +{\n> +       if (tensor->dims->size != expectedDimsSize)\n> +               return false;\n> +\n> +       for (int i = 0; i < tensor->dims->size; i++) {\n> +               if (tensor->dims->data[i] != expectedDims[i]) {\n> +                       return false;\n> +               }\n> +       }\n> +       return true;\n> +}\n> +\n> +static std::string buildDimString(const int *dims, const int dimsSize)\n> +{\n> +       std::string s = \"[\";\n> +       for (int i = 0; i < dimsSize; i++) {\n> +               s += std::to_string(dims[i]);\n> +               if (i < dimsSize - 1)\n> +                       s += \",\";\n> +               else\n> +                       s += \"]\";\n> +       }\n> +       return s;\n> +}\n> +\n> +void AwbNN::loadModel()\n> +{\n> +       std::string modelPath;\n> +       if (getTarget() == \"bcm2835\") {\n> +               modelPath = \"/ipa/rpi/vc4/awb_model.tflite\";\n> +       } else {\n> +               modelPath = \"/ipa/rpi/pisp/awb_model.tflite\";\n> +       }\n> +\n> +       if (nnConfig_.model.empty()) {\n> +               std::string root = utils::libcameraSourcePath();\n> +               if (!root.empty()) {\n> +                       modelPath = root + modelPath;\n> +               } else {\n> +                       modelPath = LIBCAMERA_DATA_DIR + modelPath;\n> +               }\n> +\n> +               if (!File::exists(modelPath)) {\n> +                       LOG(RPiAwb, Error) << \"No model file found in standard locations\";\n> +                       nnConfig_.enableNn = false;\n> +                       return;\n> +               }\n> +       } else {\n> +               modelPath = nnConfig_.model;\n> +       }\n> +\n> +       LOG(RPiAwb, Debug) << \"Attempting to load model from: \" << modelPath;\n> +\n> +       model_ = tflite::FlatBufferModel::BuildFromFile(modelPath.c_str());\n> +\n> +       if (!model_) {\n> +               LOG(RPiAwb, Error) << \"Failed to load model from \" << modelPath;\n> +               nnConfig_.enableNn = false;\n> +               return;\n> +       }\n> +\n> +       tflite::MutableOpResolver resolver;\n> +       tflite::ops::builtin::BuiltinOpResolver builtin_resolver;\n> +       resolver.AddAll(builtin_resolver);\n> +       tflite::InterpreterBuilder(*model_, resolver)(&interpreter_);\n> +       if (!interpreter_) {\n> +               LOG(RPiAwb, Error) << \"Failed to build interpreter for model \" << nnConfig_.model;\n> +               nnConfig_.enableNn = false;\n> +               return;\n> +       }\n> +\n> +       interpreter_->AllocateTensors();\n> +       TfLiteTensor *inputTensor = interpreter_->input_tensor(0);\n> +       TfLiteTensor *inputLuxTensor = interpreter_->input_tensor(1);\n> +       TfLiteTensor *outputTensor = interpreter_->output_tensor(0);\n> +       if (!inputTensor || !inputLuxTensor || !outputTensor) {\n> +               LOG(RPiAwb, Error) << \"Model missing input or output tensor\";\n> +               nnConfig_.enableNn = false;\n> +               return;\n> +       }\n> +\n> +       const int expectedInputDims[] = { 1, (int)zoneSize_.height, (int)zoneSize_.width, 3 };\n> +       const int expectedInputLuxDims[] = { 1 };\n> +       const int expectedOutputDims[] = { 1 };\n> +\n> +       if (!checkTensorShape(inputTensor, expectedInputDims, 4)) {\n> +               LOG(RPiAwb, Error) << \"Model input tensor dimension mismatch. Expected: \" << buildDimString(expectedInputDims, 4)\n> +                                  << \", Got: \" << buildDimString(inputTensor->dims->data, inputTensor->dims->size);\n> +               nnConfig_.enableNn = false;\n> +               return;\n> +       }\n> +\n> +       if (!checkTensorShape(inputLuxTensor, expectedInputLuxDims, 1)) {\n> +               LOG(RPiAwb, Error) << \"Model input lux tensor dimension mismatch. Expected: \" << buildDimString(expectedInputLuxDims, 1)\n> +                                  << \", Got: \" << buildDimString(inputLuxTensor->dims->data, inputLuxTensor->dims->size);\n> +               nnConfig_.enableNn = false;\n> +               return;\n> +       }\n> +\n> +       if (!checkTensorShape(outputTensor, expectedOutputDims, 1)) {\n> +               LOG(RPiAwb, Error) << \"Model output tensor dimension mismatch. Expected: \" << buildDimString(expectedOutputDims, 1)\n> +                                  << \", Got: \" << buildDimString(outputTensor->dims->data, outputTensor->dims->size);\n> +               nnConfig_.enableNn = false;\n> +               return;\n> +       }\n> +\n> +       if (inputTensor->type != kTfLiteFloat32 || inputLuxTensor->type != kTfLiteFloat32 || outputTensor->type != kTfLiteFloat32) {\n> +               LOG(RPiAwb, Error) << \"Model input and output tensors must be float32\";\n> +               nnConfig_.enableNn = false;\n> +               return;\n> +       }\n> +\n> +       LOG(RPiAwb, Info) << \"Model loaded successfully from \" << modelPath;\n> +       LOG(RPiAwb, Debug) << \"Model validation successful - Input Image: \"\n> +                          << buildDimString(expectedInputDims, 4)\n> +                          << \", Input Lux: \" << buildDimString(expectedInputLuxDims, 1)\n> +                          << \", Output: \" << buildDimString(expectedOutputDims, 1) << \" floats\";\n> +}\n> +\n> +void AwbNN::initialise()\n> +{\n> +       Awb::initialise();\n> +\n> +       if (nnConfig_.enableNn) {\n> +               loadModel();\n> +               if (!nnConfig_.enableNn) {\n> +                       LOG(RPiAwb, Warning) << \"Neural Network AWB failed to load - switch to Grey method\";\n> +                       config_.greyWorld = true;\n> +                       config_.sensitivityR = config_.sensitivityB = 1.0;\n> +               }\n> +       }\n> +}\n> +\n> +void AwbNN::prepareStats()\n> +{\n> +       zones_.clear();\n> +       /*\n> +        * LSC has already been applied to the stats in this pipeline, so stop\n> +        * any LSC compensation.  We also ignore config_.fast in this version.\n> +        */\n> +       generateStats(zones_, statistics_, 0.0, 0.0, getGlobalMetadata(), 0.0, 0.0, 0.0);\n> +       /*\n> +        * apply sensitivities, so values appear to come from our \"canonical\"\n> +        * sensor.\n> +        */\n> +       for (auto &zone : zones_) {\n> +               zone.R *= config_.sensitivityR;\n> +               zone.B *= config_.sensitivityB;\n> +       }\n> +}\n> +\n> +void AwbNN::transverseSearch(double t, double &r, double &b)\n> +{\n> +       int spanR = -1, spanB = -1;\n> +       config_.ctR.eval(t, &spanR);\n> +       config_.ctB.eval(t, &spanB);\n> +\n> +       const int diff = 10;\n> +       double rDiff = config_.ctR.eval(t + diff, &spanR) -\n> +                      config_.ctR.eval(t - diff, &spanR);\n> +       double bDiff = config_.ctB.eval(t + diff, &spanB) -\n> +                      config_.ctB.eval(t - diff, &spanB);\n> +\n> +       ipa::Pwl::Point transverse({ bDiff, -rDiff });\n> +       if (transverse.length2() < 1e-6)\n> +               return;\n> +\n> +       transverse = transverse / transverse.length();\n> +       double transverseRange = config_.transverseNeg + config_.transversePos;\n> +       const int maxNumDeltas = 12;\n> +       int numDeltas = floor(transverseRange * 100 + 0.5) + 1;\n> +       numDeltas = numDeltas < 3 ? 3 : (numDeltas > maxNumDeltas ? maxNumDeltas : numDeltas);\n> +\n> +       ipa::Pwl::Point points[maxNumDeltas];\n> +       int bestPoint = 0;\n> +\n> +       for (int i = 0; i < numDeltas; i++) {\n> +               points[i][0] = -config_.transverseNeg +\n> +                              (transverseRange * i) / (numDeltas - 1);\n> +               ipa::Pwl::Point rbTest = ipa::Pwl::Point({ r, b }) +\n> +                                        transverse * points[i].x();\n> +               double rTest = rbTest.x(), bTest = rbTest.y();\n> +               double gainR = 1 / rTest, gainB = 1 / bTest;\n> +               double delta2Sum = computeDelta2Sum(gainR, gainB, 0.0, 0.0);\n> +               points[i][1] = delta2Sum;\n> +               if (points[i].y() < points[bestPoint].y())\n> +                       bestPoint = i;\n> +       }\n> +\n> +       bestPoint = std::clamp(bestPoint, 1, numDeltas - 2);\n> +       ipa::Pwl::Point rbBest = ipa::Pwl::Point({ r, b }) +\n> +                                transverse * interpolateQuadatric(points[bestPoint - 1],\n> +                                                                  points[bestPoint],\n> +                                                                  points[bestPoint + 1]);\n> +       double rBest = rbBest.x(), bBest = rbBest.y();\n> +\n> +       r = rBest, b = bBest;\n> +}\n> +\n> +AwbNN::RGB AwbNN::processZone(AwbNN::RGB zone, float redGain, float blueGain)\n> +{\n> +       /*\n> +        * Renders the pixel at canonical network colour temperature\n> +        */\n> +       RGB zoneGains = zone;\n> +\n> +       zoneGains.R *= redGain;\n> +       zoneGains.G *= 1.0;\n> +       zoneGains.B *= blueGain;\n> +\n> +       RGB zoneCcm;\n> +\n> +       zoneCcm.R = nnConfig_.ccm[0] * zoneGains.R + nnConfig_.ccm[1] * zoneGains.G + nnConfig_.ccm[2] * zoneGains.B;\n> +       zoneCcm.G = nnConfig_.ccm[3] * zoneGains.R + nnConfig_.ccm[4] * zoneGains.G + nnConfig_.ccm[5] * zoneGains.B;\n> +       zoneCcm.B = nnConfig_.ccm[6] * zoneGains.R + nnConfig_.ccm[7] * zoneGains.G + nnConfig_.ccm[8] * zoneGains.B;\n> +\n> +       return zoneCcm;\n> +}\n> +\n> +void AwbNN::awbNN()\n> +{\n> +       float *inputData = interpreter_->typed_input_tensor<float>(0);\n> +       float *inputLux = interpreter_->typed_input_tensor<float>(1);\n> +\n> +       float redGain = 1.0 / config_.ctR.eval(kNetworkCanonicalCT);\n> +       float blueGain = 1.0 / config_.ctB.eval(kNetworkCanonicalCT);\n> +\n> +       for (uint i = 0; i < zoneSize_.height; i++) {\n> +               for (uint j = 0; j < zoneSize_.width; j++) {\n> +                       uint zoneIdx = i * zoneSize_.width + j;\n> +\n> +                       RGB processedZone = processZone(zones_[zoneIdx] * (1.0 / 65535), redGain, blueGain);\n> +                       uint baseIdx = zoneIdx * 3;\n> +\n> +                       inputData[baseIdx + 0] = static_cast<float>(processedZone.R);\n> +                       inputData[baseIdx + 1] = static_cast<float>(processedZone.G);\n> +                       inputData[baseIdx + 2] = static_cast<float>(processedZone.B);\n> +               }\n> +       }\n> +\n> +       inputLux[0] = static_cast<float>(lux_);\n> +\n> +       TfLiteStatus status = interpreter_->Invoke();\n> +       if (status != kTfLiteOk) {\n> +               LOG(RPiAwb, Error) << \"Model inference failed with status: \" << status;\n> +               return;\n> +       }\n> +\n> +       float *outputData = interpreter_->typed_output_tensor<float>(0);\n> +\n> +       double t = outputData[0];\n> +\n> +       LOG(RPiAwb, Debug) << \"Model output temperature: \" << t;\n> +\n> +       t = std::clamp(t, mode_->ctLo, mode_->ctHi);\n> +\n> +       double r = config_.ctR.eval(t);\n> +       double b = config_.ctB.eval(t);\n> +\n> +       transverseSearch(t, r, b);\n> +\n> +       LOG(RPiAwb, Debug) << \"After transverse search: Temperature: \" << t << \" Red gain: \" << 1.0 / r << \" Blue gain: \" << 1.0 / b;\n> +\n> +       asyncResults_.temperatureK = t;\n> +       asyncResults_.gainR = 1.0 / r * config_.sensitivityR;\n> +       asyncResults_.gainG = 1.0;\n> +       asyncResults_.gainB = 1.0 / b * config_.sensitivityB;\n> +}\n> +\n> +void AwbNN::doAwb()\n> +{\n> +       prepareStats();\n> +       if (zones_.size() == (zoneSize_.width * zoneSize_.height) && nnConfig_.enableNn)\n> +               awbNN();\n> +       else\n> +               awbGrey();\n> +       statistics_.reset();\n> +}\n> +\n> +/* Register algorithm with the system. */\n> +static Algorithm *create(Controller *controller)\n> +{\n> +       return (Algorithm *)new AwbNN(controller);\n> +}\n> +static RegisterAlgorithm reg(NAME, &create);\n> +\n> +} /* namespace RPiController */\n> --\n> 2.47.3\n>","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 539FDBD1F1\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu, 11 Dec 2025 15:37:09 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 5CC4561617;\n\tThu, 11 Dec 2025 16:37:08 +0100 (CET)","from mail-qt1-x830.google.com (mail-qt1-x830.google.com\n\t[IPv6:2607:f8b0:4864:20::830])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 1A09461603\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 11 Dec 2025 16:37:06 +0100 (CET)","by mail-qt1-x830.google.com with SMTP id\n\td75a77b69052e-4ee1879e6d9so2402441cf.1\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 11 Dec 2025 07:37:05 -0800 (PST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=raspberrypi.com header.i=@raspberrypi.com\n\theader.b=\"TFb7NnI0\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1765467425; x=1766072225;\n\tdarn=lists.libcamera.org; \n\th=cc:to:subject:message-id:date:from:in-reply-to:references\n\t:mime-version:from:to:cc:subject:date:message-id:reply-to;\n\tbh=uDukuWVIpgeFhioLRqHNSwcs6Y2DcmFJd+LuW02SBv0=;\n\tb=TFb7NnI0QUd5ZS8YMBY1Sf9trOC4Ea2h7TELj5s/NyW0N60X4NjHG8aJ702LGXV/i1\n\tZ1rZkef9seg1LJc2qRuslY4RroHHHkIV2mL6QiZdKD026gdacZ3WLFw6yx2l5oohAcod\n\tUqcyR3HUhog0Dao2u+lnHFHypb2M1sm73ifMPXsk5Mt33BYuwO+2WEgeCHyCa8eKCvOp\n\tpCKwDM/909CwdrDv2uZLqmarILw7KkXDbxhfGiKdw5ws071Mv/Iw4Rmn0lAig8eu3DU7\n\tyxZOfetnFRQzbOn2KL894Fwk7UkYwGO+z2edjSsDEAtP8vTmA3/bDLuphqvXb69/rqzp\n\tqxzA==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1765467425; x=1766072225;\n\th=cc:to:subject:message-id:date:from:in-reply-to:references\n\t:mime-version:x-gm-gg:x-gm-message-state:from:to:cc:subject:date\n\t:message-id:reply-to;\n\tbh=uDukuWVIpgeFhioLRqHNSwcs6Y2DcmFJd+LuW02SBv0=;\n\tb=VUvptzbtFli4TobLjT0kNkvjn5kPAUPig1cyQoyt6b8qcPaYkBxHsMhGJNUH+aZy6u\n\thQIilWrhJT4BTHM5mZRNuek6DrV9zi97nOYiaz2qABllJhvErc5dpOMq3oSrlqWUINSr\n\tzEkgZPOEzXe265LMH9IR2O0WvF/WfjsC/K2vKQ7H965iEqsKbTk0xOD4FUXK7pqQnIKX\n\tfhl1vkBpTktAxe9Gg5fbTn3qptyB8YPBj0bp7TwosvxD3tpleh+8T0h25E2Ip2ZoD77g\n\tZNsU9vV27Dd+E0DUAYmr6GRAldqge0ys+y3N4Bopf+WqFHu3DgBLkPHbupvQAfM0NfjL\n\tUaSg==","X-Gm-Message-State":"AOJu0YxbddtSjCD+Yk7d/7mks1bRIAuVepQnO/Z9RxVYNd5uGWeLG3Hl\n\tAwXCZxkojgK0DEqM3pKB+y9p79A5aZl0EkMYSSa888y7X3tUYskFTc/JbvSzApodiF7w7YkuwXq\n\t52QJCKgb/6ag5BY2mxjSPLjapRBkjIyW/lAGtfkoHe0s5huqS3ntRWfg=","X-Gm-Gg":"AY/fxX4ABr7GT+iVKCASp53mRC+L5NID6tb9oyTT5sI0yihWcasZp8HWR8CFlVb2M8v\n\tvVaJjjXbcck7FOSKL7/pHao/M0wlM7uyhDhT5k3y7Tr7N0T4YiPiW8akxUH96nz4+wVnIuiDk7W\n\t5EsBzARciLgJePfKvCIK+fYIKXgMtO+SZklvAcfDnADDJFUkw5tGySAHGD+C5omUsHDI9YlQdJE\n\td/hpn/TbA/x9BxrN256wJA+zuCogQG2xOG0SuN7T2o43I5qCL2TINpQFHApfp7kr9Q4HC5ptcJM\n\tNrUNmmSmv6iZ5craNY2I/VDnDC6H/joi8iNMStS1o2i5ZdN6b6ro5Fpka9olHS9fgP331ZXHVdJ\n\tzMhs0Z1v7nir9oPugfSmA0+s=","X-Google-Smtp-Source":"AGHT+IFDfdA/TyM51ziNOQ6kjNRRBOxxW62RVme0nZJt2wR98EshHfPTfYkU3tpv8IRlk893js6DmU8PXdWzI2SUXlQ=","X-Received":"by 2002:a05:622a:5a0d:b0:4ee:1f24:8c43 with SMTP id\n\td75a77b69052e-4f1b19d4ba9mr83372311cf.31.1765467424473;\n\tThu, 11 Dec 2025 07:37:04 -0800 (PST)","MIME-Version":"1.0","References":"<20251211142824.26635-1-david.plowman@raspberrypi.com>\n\t<20251211142824.26635-3-david.plowman@raspberrypi.com>","In-Reply-To":"<20251211142824.26635-3-david.plowman@raspberrypi.com>","From":"David Plowman <david.plowman@raspberrypi.com>","Date":"Thu, 11 Dec 2025 15:36:53 +0000","X-Gm-Features":"AQt7F2pEnv6CIYCLyGlmUAJhMI-bcgGSCKEANy9lGyst2-mUHxKIFkWH3SXaqIc","Message-ID":"<CAHW6GYJcjEuBCt+EGYF+H0dedHJ=b3dw62Bs0ygq5qcXh7SHYg@mail.gmail.com>","Subject":"Re: [PATCH v2 2/4] ipa: rpi: controller: awb: Add Neural Network AWB","To":"libcamera-devel@lists.libcamera.org","Cc":"Peter Bailey <peter.bailey@raspberrypi.com>","Content-Type":"text/plain; charset=\"UTF-8\"","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}}]