[{"id":33713,"web_url":"https://patchwork.libcamera.org/comment/33713/","msgid":"<174298558278.670680.16777298742932773810@ping.linuxembedded.co.uk>","date":"2025-03-26T10:39:42","subject":"Re: [PATCH v8 10/10] libcamera: software_isp: Apply CCM in\n\tdebayering","submitter":{"id":4,"url":"https://patchwork.libcamera.org/api/people/4/","name":"Kieran Bingham","email":"kieran.bingham@ideasonboard.com"},"content":"Quoting Milan Zamazal (2025-03-26 09:08:47)\n> This patch applies color correction matrix (CCM) in debayering if the\n> CCM is specified.  Not using CCM must still be supported for performance\n> reasons.\n> \n> The CCM is applied as follows:\n> \n>   [r1 g1 b1]   [r]\n>   [r2 g2 b2] * [g]\n>   [r3 g3 b3]   [b]\n> \n> The CCM matrix (the left side of the multiplication) is constant during\n> single frame processing, while the input pixel (the right side) changes.\n> Because each of the color channels is only 8-bit in software ISP, we can\n> make 9 lookup tables with 256 input values for multiplications of each\n> of the r_i, g_i, b_i values.  This way we don't have to multiply each\n> pixel, we can use table lookups and additions instead.  Gamma (which is\n> non-linear and thus cannot be a part of the 9 lookup tables values) is\n> applied on the final values rounded to integers using another lookup\n> table.\n> \n> Because the changing part is the pixel value with three color elements,\n> only three dynamic table lookups are needed.  We use three lookup tables\n> to represent the multiplied matrix values, each of the tables\n> corresponding to the given matrix column and pixel color.\n> \n> We use int16_t to store the precomputed multiplications.  This seems to\n> be noticeably (>10%) faster than `float' for the price of slightly less\n> accuracy and it covers the range of values that sane CCMs produce.  The\n> selection and structure of data is performance critical, for example\n> using bytes would add significant (>10%) speedup but would be too short\n> to cover the value range.\n> \n> The color lookup tables can be represented either as unions,\n> accommodating tables for both the CCM and non-CCM cases, or as separate\n> tables for each of the cases, leaving the tables for the other case\n> unused.  The latter is selected as a matter of preference.\n> \n> The tables are copied (as before), which is not elegant but also not a\n> big problem.  There are patches posted that use shared buffers for\n> parameters passing in software ISP (see software ISP TODO #5) and they\n> can be adjusted for the new parameter format.\n> \n> Color gains from white balance are supposed not to be a part of the\n> specified CCM.  They are applied on it using matrix multiplication,\n> which is simple and in correspondence with future additions in the form\n> of matrix multiplication, like saturation adjustment.\n> \n> With this patch, the reported per-frame slowdown when applying CCM is\n> about 45% on Debix Model A and about 75% on TI AM69 SK.\n> \n> Using std::clamp in debayering adds some performance penalty (a few\n> percent).  The clamping is necessary to eliminate out of range values\n> possibly produced by the CCM.  If it could be avoided by adjusting the\n> precomputed tables some way then performance could be improved a bit.\n> \n> Signed-off-by: Milan Zamazal <mzamazal@redhat.com>\n> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n\nReviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>\n\n> ---\n>  .../internal/software_isp/debayer_params.h    | 36 ++++++++++--\n>  src/ipa/simple/algorithms/lut.cpp             | 53 +++++++++++++----\n>  src/ipa/simple/algorithms/lut.h               |  1 +\n>  src/libcamera/software_isp/debayer.cpp        | 57 ++++++++++++++++++-\n>  src/libcamera/software_isp/debayer_cpu.cpp    | 53 +++++++++++++----\n>  src/libcamera/software_isp/debayer_cpu.h      | 10 +++-\n>  6 files changed, 177 insertions(+), 33 deletions(-)\n> \n> diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h\n> index 7d8fdd48..217cd5d9 100644\n> --- a/include/libcamera/internal/software_isp/debayer_params.h\n> +++ b/include/libcamera/internal/software_isp/debayer_params.h\n> @@ -1,6 +1,6 @@\n>  /* SPDX-License-Identifier: LGPL-2.1-or-later */\n>  /*\n> - * Copyright (C) 2023, 2024 Red Hat Inc.\n> + * Copyright (C) 2023-2025 Red Hat Inc.\n>   *\n>   * Authors:\n>   * Hans de Goede <hdegoede@redhat.com>\n> @@ -18,11 +18,37 @@ namespace libcamera {\n>  struct DebayerParams {\n>         static constexpr unsigned int kRGBLookupSize = 256;\n>  \n> -       using ColorLookupTable = std::array<uint8_t, kRGBLookupSize>;\n> +       struct CcmColumn {\n> +               int16_t r;\n> +               int16_t g;\n> +               int16_t b;\n> +       };\n>  \n> -       ColorLookupTable red;\n> -       ColorLookupTable green;\n> -       ColorLookupTable blue;\n> +       using LookupTable = std::array<uint8_t, kRGBLookupSize>;\n> +       using CcmLookupTable = std::array<CcmColumn, kRGBLookupSize>;\n> +\n> +       /*\n> +        * Color lookup tables when CCM is not used.\n> +        *\n> +        * Each color of a debayered pixel is amended by the corresponding\n> +        * value in the given table.\n> +        */\n> +       LookupTable red;\n> +       LookupTable green;\n> +       LookupTable blue;\n> +\n> +       /*\n> +        * Color and gamma lookup tables when CCM is used.\n> +        *\n> +        * Each of the CcmLookupTable's corresponds to a CCM column; together they\n> +        * make a complete 3x3 CCM lookup table. The CCM is applied on debayered\n> +        * pixels and then the gamma lookup table is used to set the resulting\n> +        * values of all the three colors.\n> +        */\n> +       CcmLookupTable redCcm;\n> +       CcmLookupTable greenCcm;\n> +       CcmLookupTable blueCcm;\n> +       LookupTable gammaLut;\n>  };\n>  \n>  } /* namespace libcamera */\n> diff --git a/src/ipa/simple/algorithms/lut.cpp b/src/ipa/simple/algorithms/lut.cpp\n> index 3a3daed7..a06cdeba 100644\n> --- a/src/ipa/simple/algorithms/lut.cpp\n> +++ b/src/ipa/simple/algorithms/lut.cpp\n> @@ -1,6 +1,6 @@\n>  /* SPDX-License-Identifier: LGPL-2.1-or-later */\n>  /*\n> - * Copyright (C) 2024, Red Hat Inc.\n> + * Copyright (C) 2024-2025, Red Hat Inc.\n>   *\n>   * Color lookup tables construction\n>   */\n> @@ -80,6 +80,11 @@ void Lut::updateGammaTable(IPAContext &context)\n>         context.activeState.gamma.contrast = contrast;\n>  }\n>  \n> +int16_t Lut::ccmValue(unsigned int i, float ccm) const\n> +{\n> +       return std::round(i * ccm);\n> +}\n> +\n>  void Lut::prepare(IPAContext &context,\n>                   [[maybe_unused]] const uint32_t frame,\n>                   [[maybe_unused]] IPAFrameContext &frameContext,\n> @@ -91,22 +96,46 @@ void Lut::prepare(IPAContext &context,\n>          * observed, it's not permanently prone to minor fluctuations or\n>          * rounding errors.\n>          */\n> -       if (context.activeState.gamma.blackLevel != context.activeState.blc.level ||\n> -           context.activeState.gamma.contrast != context.activeState.knobs.contrast)\n> +       const bool gammaUpdateNeeded =\n> +               context.activeState.gamma.blackLevel != context.activeState.blc.level ||\n> +               context.activeState.gamma.contrast != context.activeState.knobs.contrast;\n> +       if (gammaUpdateNeeded)\n>                 updateGammaTable(context);\n>  \n>         auto &gains = context.activeState.awb.gains;\n>         auto &gammaTable = context.activeState.gamma.gammaTable;\n>         const unsigned int gammaTableSize = gammaTable.size();\n> -\n> -       for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {\n> -               const double div = static_cast<double>(DebayerParams::kRGBLookupSize) /\n> -                                  gammaTableSize;\n> -               /* Apply gamma after gain! */\n> -               const RGB<float> lutGains = (gains * i / div).min(gammaTableSize - 1);\n> -               params->red[i] = gammaTable[static_cast<unsigned int>(lutGains.r())];\n> -               params->green[i] = gammaTable[static_cast<unsigned int>(lutGains.g())];\n> -               params->blue[i] = gammaTable[static_cast<unsigned int>(lutGains.b())];\n> +       const double div = static_cast<double>(DebayerParams::kRGBLookupSize) /\n> +                          gammaTableSize;\n> +\n> +       if (!context.ccmEnabled) {\n> +               for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {\n> +                       /* Apply gamma after gain! */\n> +                       const RGB<float> lutGains = (gains * i / div).min(gammaTableSize - 1);\n> +                       params->red[i] = gammaTable[static_cast<unsigned int>(lutGains.r())];\n> +                       params->green[i] = gammaTable[static_cast<unsigned int>(lutGains.g())];\n> +                       params->blue[i] = gammaTable[static_cast<unsigned int>(lutGains.b())];\n> +               }\n> +       } else if (context.activeState.ccm.changed || gammaUpdateNeeded) {\n> +               Matrix<float, 3, 3> gainCcm = { { gains.r(), 0, 0,\n> +                                                 0, gains.g(), 0,\n> +                                                 0, 0, gains.b() } };\n> +               auto ccm = gainCcm * context.activeState.ccm.ccm;\n> +               auto &red = params->redCcm;\n> +               auto &green = params->greenCcm;\n> +               auto &blue = params->blueCcm;\n> +               for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {\n> +                       red[i].r = ccmValue(i, ccm[0][0]);\n> +                       red[i].g = ccmValue(i, ccm[1][0]);\n> +                       red[i].b = ccmValue(i, ccm[2][0]);\n> +                       green[i].r = ccmValue(i, ccm[0][1]);\n> +                       green[i].g = ccmValue(i, ccm[1][1]);\n> +                       green[i].b = ccmValue(i, ccm[2][1]);\n> +                       blue[i].r = ccmValue(i, ccm[0][2]);\n> +                       blue[i].g = ccmValue(i, ccm[1][2]);\n> +                       blue[i].b = ccmValue(i, ccm[2][2]);\n> +                       params->gammaLut[i] = gammaTable[i / div];\n> +               }\n>         }\n>  }\n>  \n> diff --git a/src/ipa/simple/algorithms/lut.h b/src/ipa/simple/algorithms/lut.h\n> index 889f864b..77324800 100644\n> --- a/src/ipa/simple/algorithms/lut.h\n> +++ b/src/ipa/simple/algorithms/lut.h\n> @@ -33,6 +33,7 @@ public:\n>  \n>  private:\n>         void updateGammaTable(IPAContext &context);\n> +       int16_t ccmValue(unsigned int i, float ccm) const;\n>  };\n>  \n>  } /* namespace ipa::soft::algorithms */\n> diff --git a/src/libcamera/software_isp/debayer.cpp b/src/libcamera/software_isp/debayer.cpp\n> index 34e42201..e9e18c48 100644\n> --- a/src/libcamera/software_isp/debayer.cpp\n> +++ b/src/libcamera/software_isp/debayer.cpp\n> @@ -1,7 +1,7 @@\n>  /* SPDX-License-Identifier: LGPL-2.1-or-later */\n>  /*\n>   * Copyright (C) 2023, Linaro Ltd\n> - * Copyright (C) 2023, 2024 Red Hat Inc.\n> + * Copyright (C) 2023-2025 Red Hat Inc.\n>   *\n>   * Authors:\n>   * Hans de Goede <hdegoede@redhat.com>\n> @@ -24,8 +24,39 @@ namespace libcamera {\n>   */\n>  \n>  /**\n> - * \\typedef DebayerParams::ColorLookupTable\n> - * \\brief Type of the lookup tables for red, green, blue values\n> + * \\struct DebayerParams::CcmColumn\n> + * \\brief Type of a single column of a color correction matrix (CCM)\n> + *\n> + * When multiplying an input pixel, columns in the CCM correspond to the red,\n> + * green or blue component of input pixel values, while rows correspond to the\n> + * red, green or blue components of the output pixel values. The members of the\n> + * CcmColumn structure are named after the colour components of the output pixel\n> + * values they correspond to.\n> + */\n> +\n> +/**\n> + * \\var DebayerParams::CcmColumn::r\n> + * \\brief Red (first) component of a CCM column\n> + */\n> +\n> +/**\n> + * \\var DebayerParams::CcmColumn::g\n> + * \\brief Green (second) component of a CCM column\n> + */\n> +\n> +/**\n> + * \\var DebayerParams::CcmColumn::b\n> + * \\brief Blue (third) component of a CCM column\n> + */\n> +\n> +/**\n> + * \\typedef DebayerParams::LookupTable\n> + * \\brief Type of the lookup tables for single lookup values\n> + */\n> +\n> +/**\n> + * \\typedef DebayerParams::CcmLookupTable\n> + * \\brief Type of the CCM lookup tables for red, green, blue values\n>   */\n>  \n>  /**\n> @@ -43,6 +74,26 @@ namespace libcamera {\n>   * \\brief Lookup table for blue color, mapping input values to output values\n>   */\n>  \n> +/**\n> + * \\var DebayerParams::redCcm\n> + * \\brief Lookup table for the CCM red column, mapping input values to output values\n> + */\n> +\n> +/**\n> + * \\var DebayerParams::greenCcm\n> + * \\brief Lookup table for the CCM green column, mapping input values to output values\n> + */\n> +\n> +/**\n> + * \\var DebayerParams::blueCcm\n> + * \\brief Lookup table for the CCM blue column, mapping input values to output values\n> + */\n> +\n> +/**\n> + * \\var DebayerParams::gammaLut\n> + * \\brief Gamma lookup table used with color correction matrix\n> + */\n> +\n>  /**\n>   * \\class Debayer\n>   * \\brief Base debayering class\n> diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp\n> index 0cd03a8f..66f6038c 100644\n> --- a/src/libcamera/software_isp/debayer_cpu.cpp\n> +++ b/src/libcamera/software_isp/debayer_cpu.cpp\n> @@ -1,7 +1,7 @@\n>  /* SPDX-License-Identifier: LGPL-2.1-or-later */\n>  /*\n>   * Copyright (C) 2023, Linaro Ltd\n> - * Copyright (C) 2023, Red Hat Inc.\n> + * Copyright (C) 2023-2025 Red Hat Inc.\n>   *\n>   * Authors:\n>   * Hans de Goede <hdegoede@redhat.com>\n> @@ -11,9 +11,11 @@\n>  \n>  #include \"debayer_cpu.h\"\n>  \n> +#include <algorithm>\n>  #include <stdlib.h>\n>  #include <sys/ioctl.h>\n>  #include <time.h>\n> +#include <utility>\n>  \n>  #include <linux/dma-buf.h>\n>  \n> @@ -51,8 +53,12 @@ DebayerCpu::DebayerCpu(std::unique_ptr<SwStatsCpu> stats)\n>         enableInputMemcpy_ = true;\n>  \n>         /* Initialize color lookup tables */\n> -       for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++)\n> +       for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {\n>                 red_[i] = green_[i] = blue_[i] = i;\n> +               redCcm_[i] = { static_cast<int16_t>(i), 0, 0 };\n> +               greenCcm_[i] = { 0, static_cast<int16_t>(i), 0 };\n> +               blueCcm_[i] = { 0, 0, static_cast<int16_t>(i) };\n> +       }\n>  }\n>  \n>  DebayerCpu::~DebayerCpu() = default;\n> @@ -62,12 +68,24 @@ DebayerCpu::~DebayerCpu() = default;\n>         const pixel_t *curr = (const pixel_t *)src[1] + xShift_; \\\n>         const pixel_t *next = (const pixel_t *)src[2] + xShift_;\n>  \n> -#define STORE_PIXEL(b, g, r)        \\\n> -       *dst++ = blue_[b];          \\\n> -       *dst++ = green_[g];         \\\n> -       *dst++ = red_[r];           \\\n> -       if constexpr (addAlphaByte) \\\n> -               *dst++ = 255;       \\\n> +#define GAMMA(value) \\\n> +       *dst++ = gammaLut_[std::clamp(value, 0, static_cast<int>(gammaLut_.size()) - 1)]\n> +\n> +#define STORE_PIXEL(b_, g_, r_)                                        \\\n> +       if constexpr (ccmEnabled) {                                    \\\n> +               const DebayerParams::CcmColumn &blue = blueCcm_[b_];   \\\n> +               const DebayerParams::CcmColumn &green = greenCcm_[g_]; \\\n> +               const DebayerParams::CcmColumn &red = redCcm_[r_];     \\\n> +               GAMMA(blue.b + green.b + red.b);                       \\\n> +               GAMMA(blue.g + green.g + red.g);                       \\\n> +               GAMMA(blue.r + green.r + red.r);                       \\\n> +       } else {                                                       \\\n> +               *dst++ = blue_[b_];                                    \\\n> +               *dst++ = green_[g_];                                   \\\n> +               *dst++ = red_[r_];                                     \\\n> +       }                                                              \\\n> +       if constexpr (addAlphaByte)                                    \\\n> +               *dst++ = 255;                                          \\\n>         x++;\n>  \n>  /*\n> @@ -755,8 +773,23 @@ void DebayerCpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output\n>                 dmaSyncers.emplace_back(plane.fd, DmaSyncer::SyncType::Write);\n>  \n>         green_ = params.green;\n> -       red_ = swapRedBlueGains_ ? params.blue : params.red;\n> -       blue_ = swapRedBlueGains_ ? params.red : params.blue;\n> +       greenCcm_ = params.greenCcm;\n> +       if (swapRedBlueGains_) {\n> +               red_ = params.blue;\n> +               blue_ = params.red;\n> +               redCcm_ = params.blueCcm;\n> +               blueCcm_ = params.redCcm;\n> +               for (unsigned int i = 0; i < 256; i++) {\n> +                       std::swap(redCcm_[i].r, redCcm_[i].b);\n> +                       std::swap(blueCcm_[i].r, blueCcm_[i].b);\n> +               }\n> +       } else {\n> +               red_ = params.red;\n> +               blue_ = params.blue;\n> +               redCcm_ = params.redCcm;\n> +               blueCcm_ = params.blueCcm;\n> +       }\n> +       gammaLut_ = params.gammaLut;\n>  \n>         /* Copy metadata from the input buffer */\n>         FrameMetadata &metadata = output->_d()->metadata();\n> diff --git a/src/libcamera/software_isp/debayer_cpu.h b/src/libcamera/software_isp/debayer_cpu.h\n> index 21c08a2d..926195e9 100644\n> --- a/src/libcamera/software_isp/debayer_cpu.h\n> +++ b/src/libcamera/software_isp/debayer_cpu.h\n> @@ -138,9 +138,13 @@ private:\n>         /* Max. supported Bayer pattern height is 4, debayering this requires 5 lines */\n>         static constexpr unsigned int kMaxLineBuffers = 5;\n>  \n> -       DebayerParams::ColorLookupTable red_;\n> -       DebayerParams::ColorLookupTable green_;\n> -       DebayerParams::ColorLookupTable blue_;\n> +       DebayerParams::LookupTable red_;\n> +       DebayerParams::LookupTable green_;\n> +       DebayerParams::LookupTable blue_;\n> +       DebayerParams::CcmLookupTable redCcm_;\n> +       DebayerParams::CcmLookupTable greenCcm_;\n> +       DebayerParams::CcmLookupTable blueCcm_;\n> +       DebayerParams::LookupTable gammaLut_;\n>         debayerFn debayer0_;\n>         debayerFn debayer1_;\n>         debayerFn debayer2_;\n> -- \n> 2.49.0\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 07D97C3213\n\tfor <parsemail@patchwork.libcamera.org>;\n\tWed, 26 Mar 2025 10:39:48 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id B7F3D6895D;\n\tWed, 26 Mar 2025 11:39:47 +0100 (CET)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 959E76894B\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 26 Mar 2025 11:39:45 +0100 (CET)","from pendragon.ideasonboard.com\n\t(cpc89244-aztw30-2-0-cust6594.18-1.cable.virginm.net [86.31.185.195])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id D0722475;\n\tWed, 26 Mar 2025 11:37:57 +0100 (CET)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"At2SMD27\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1742985477;\n\tbh=Ow+1JBws4ujAAMjj4I7WE2AzRLHOozKBEMLQ0bjLVvE=;\n\th=In-Reply-To:References:Subject:From:Cc:To:Date:From;\n\tb=At2SMD27Xjim5IEZG6mfkvG148iZTCMp+pIvEg1gVy8BBfbOGjV73M92Lstr6y5M0\n\t6EfBFTlc2e3iQsxVzRCwHr3HHYnJt14AoGpphTuVwC96E5RCYlheFr44SlSodLgZ1N\n\tVfBnFFXRV8tZIMFL+OHTH9i+87LYlBiVbGiUSZlg=","Content-Type":"text/plain; charset=\"utf-8\"","MIME-Version":"1.0","Content-Transfer-Encoding":"quoted-printable","In-Reply-To":"<20250326090849.15494-11-mzamazal@redhat.com>","References":"<20250326090849.15494-1-mzamazal@redhat.com>\n\t<20250326090849.15494-11-mzamazal@redhat.com>","Subject":"Re: [PATCH v8 10/10] libcamera: software_isp: Apply CCM in\n\tdebayering","From":"Kieran Bingham <kieran.bingham@ideasonboard.com>","Cc":"Milan Zamazal <mzamazal@redhat.com>,\n\tRobert Mader <robert.mader@collabora.com>,\n\tHans de Goede <hdegoede@redhat.com>, \n\tLaurent Pinchart <laurent.pinchart@ideasonboard.com>","To":"Milan Zamazal <mzamazal@redhat.com>, libcamera-devel@lists.libcamera.org","Date":"Wed, 26 Mar 2025 10:39:42 +0000","Message-ID":"<174298558278.670680.16777298742932773810@ping.linuxembedded.co.uk>","User-Agent":"alot/0.10","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>"}}]