[{"id":34253,"web_url":"https://patchwork.libcamera.org/comment/34253/","msgid":"<174739424296.530918.16089723990803615032@calcite>","date":"2025-05-16T11:17:22","subject":"Re: [PATCH v3 1/1] libcamera: software_isp: Add saturation control","submitter":{"id":17,"url":"https://patchwork.libcamera.org/api/people/17/","name":"Paul Elder","email":"paul.elder@ideasonboard.com"},"content":"Quoting Milan Zamazal (2025-05-15 18:04:31)\n> Saturation control is added on top of the colour correction matrix.  A\n> method of saturation adjustment that can be fully integrated into the\n> colour correction matrix is used.  The control is available only if Ccm\n> algorithm is enabled.\n> \n> The control uses 0.0-2.0 value range, with 1.0 being unmodified\n> saturation, 0.0 full desaturation and 2.0 quite saturated.\n> \n> The saturation is adjusted by converting to Y'CbCr colour space,\n> applying the saturation value on the colour axes, and converting back to\n> RGB.  ITU-R BT.601 conversion is used to convert between the colour\n> spaces, for no particular reason.\n> \n> The colour correction matrix is applied before gamma and the given\n> matrix is suitable for such a case.  Alternatively, the transformation\n> used in libcamera rpi ccm.cpp could be used.\n> \n> Signed-off-by: Milan Zamazal <mzamazal@redhat.com>\n> ---\n>  src/ipa/simple/algorithms/ccm.cpp | 60 +++++++++++++++++++++++++++++--\n>  src/ipa/simple/algorithms/ccm.h   | 11 ++++++\n>  src/ipa/simple/ipa_context.h      |  4 +++\n>  3 files changed, 72 insertions(+), 3 deletions(-)\n> \n> diff --git a/src/ipa/simple/algorithms/ccm.cpp b/src/ipa/simple/algorithms/ccm.cpp\n> index d5ba928d..0a98406c 100644\n> --- a/src/ipa/simple/algorithms/ccm.cpp\n> +++ b/src/ipa/simple/algorithms/ccm.cpp\n> @@ -3,7 +3,7 @@\n>   * Copyright (C) 2024, Ideas On Board\n>   * Copyright (C) 2024-2025, Red Hat Inc.\n>   *\n> - * Color correction matrix\n> + * Color correction matrix + saturation\n>   */\n>  \n>  #include \"ccm.h\"\n> @@ -13,6 +13,8 @@\n>  \n>  #include <libcamera/control_ids.h>\n>  \n> +#include \"libcamera/internal/matrix.h\"\n> +\n>  namespace {\n>  \n>  constexpr unsigned int kTemperatureThreshold = 100;\n> @@ -35,28 +37,77 @@ int Ccm::init([[maybe_unused]] IPAContext &context, const YamlObject &tuningData\n>         }\n>  \n>         context.ccmEnabled = true;\n> +       context.ctrlMap[&controls::Saturation] = ControlInfo(0.0f, 2.0f, 1.0f);\n> +\n> +       return 0;\n> +}\n> +\n> +int Ccm::configure(IPAContext &context,\n> +                  [[maybe_unused]] const IPAConfigInfo &configInfo)\n> +{\n> +       context.activeState.knobs.saturation = std::optional<double>();\n>  \n>         return 0;\n>  }\n>  \n> +void Ccm::queueRequest(typename Module::Context &context,\n> +                      [[maybe_unused]] const uint32_t frame,\n> +                      [[maybe_unused]] typename Module::FrameContext &frameContext,\n> +                      const ControlList &controls)\n> +{\n> +       const auto &saturation = controls.get(controls::Saturation);\n> +       if (saturation.has_value()) {\n> +               context.activeState.knobs.saturation = saturation;\n> +               LOG(IPASoftCcm, Debug) << \"Setting saturation to \" << saturation.value();\n> +       }\n> +}\n> +\n> +void Ccm::applySaturation(Matrix<float, 3, 3> &ccm, float saturation)\n> +{\n> +       /* https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion */\n> +       const Matrix<float, 3, 3> rgb2ycbcr{\n> +               { 0.256788235294, 0.504129411765, 0.0979058823529,\n> +                 -0.148223529412, -0.290992156863, 0.439215686275,\n> +                 0.439215686275, -0.367788235294, -0.0714274509804 }\n> +       };\n> +       const Matrix<float, 3, 3> ycbcr2rgb{\n> +               { 1.16438356164, 0, 1.59602678571,\n> +                 1.16438356164, -0.391762290094, -0.812967647235,\n> +                 1.16438356164, 2.01723214285, 0 }\n> +       };\n> +       const Matrix<float, 3, 3> saturationMatrix{\n> +               { 1, 0, 0,\n> +                 0, saturation, 0,\n> +                 0, 0, saturation }\n> +       };\n> +       ccm = ycbcr2rgb * saturationMatrix * rgb2ycbcr * ccm;\n> +}\n> +\n>  void Ccm::prepare(IPAContext &context, const uint32_t frame,\n>                   IPAFrameContext &frameContext, [[maybe_unused]] DebayerParams *params)\n>  {\n> +       auto &saturation = context.activeState.knobs.saturation;\n> +\n>         const unsigned int ct = context.activeState.awb.temperatureK;\n>  \n> -       /* Change CCM only on bigger temperature changes. */\n> +       /* Change CCM only on saturation or bigger temperature changes. */\n>         if (frame > 0 &&\n> -           utils::abs_diff(ct, lastCt_) < kTemperatureThreshold) {\n> +           utils::abs_diff(ct, lastCt_) < kTemperatureThreshold &&\n> +           saturation == lastSaturation_) {\n\nHm, at first I thought that maybe it's not a good idea to use the equivalence\noperator for floats, but I suppose that even if the value that the user submits\ngets rounded due to precision, it would still result in the same value very\ntime (if they submit the same value). I don't imagine that this equivalence\nwould cause a significant increase in unnecessary ccm operations so maybe it's\nfine.\n\nReviewed-by: Paul Elder <paul.elder@ideasonboard.com>\n\n>                 frameContext.ccm.ccm = context.activeState.ccm.ccm;\n>                 context.activeState.ccm.changed = false;\n>                 return;\n>         }\n>  \n>         lastCt_ = ct;\n> +       lastSaturation_ = saturation;\n>         Matrix<float, 3, 3> ccm = ccm_.getInterpolated(ct);\n> +       if (saturation)\n> +               applySaturation(ccm, saturation.value());\n>  \n>         context.activeState.ccm.ccm = ccm;\n>         frameContext.ccm.ccm = ccm;\n> +       frameContext.saturation = saturation;\n>         context.activeState.ccm.changed = true;\n>  }\n>  \n> @@ -67,6 +118,9 @@ void Ccm::process([[maybe_unused]] IPAContext &context,\n>                   ControlList &metadata)\n>  {\n>         metadata.set(controls::ColourCorrectionMatrix, frameContext.ccm.ccm.data());\n> +\n> +       const auto &saturation = frameContext.saturation;\n> +       metadata.set(controls::Saturation, saturation.value_or(1.0));\n>  }\n>  \n>  REGISTER_IPA_ALGORITHM(Ccm, \"Ccm\")\n> diff --git a/src/ipa/simple/algorithms/ccm.h b/src/ipa/simple/algorithms/ccm.h\n> index f4e2b85b..8279a3d5 100644\n> --- a/src/ipa/simple/algorithms/ccm.h\n> +++ b/src/ipa/simple/algorithms/ccm.h\n> @@ -7,6 +7,8 @@\n>  \n>  #pragma once\n>  \n> +#include <optional>\n> +\n>  #include \"libcamera/internal/matrix.h\"\n>  \n>  #include <libipa/interpolator.h>\n> @@ -24,6 +26,12 @@ public:\n>         ~Ccm() = default;\n>  \n>         int init(IPAContext &context, const YamlObject &tuningData) override;\n> +       int configure(IPAContext &context,\n> +                     const IPAConfigInfo &configInfo) override;\n> +       void queueRequest(typename Module::Context &context,\n> +                         const uint32_t frame,\n> +                         typename Module::FrameContext &frameContext,\n> +                         const ControlList &controls) override;\n>         void prepare(IPAContext &context,\n>                      const uint32_t frame,\n>                      IPAFrameContext &frameContext,\n> @@ -34,7 +42,10 @@ public:\n>                      ControlList &metadata) override;\n>  \n>  private:\n> +       void applySaturation(Matrix<float, 3, 3> &ccm, float saturation);\n> +\n>         unsigned int lastCt_;\n> +       std::optional<float> lastSaturation_;\n>         Interpolator<Matrix<float, 3, 3>> ccm_;\n>  };\n>  \n> diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h\n> index 88cc6c35..a471b80a 100644\n> --- a/src/ipa/simple/ipa_context.h\n> +++ b/src/ipa/simple/ipa_context.h\n> @@ -63,6 +63,7 @@ struct IPAActiveState {\n>         struct {\n>                 /* 0..2 range, 1.0 = normal */\n>                 std::optional<double> contrast;\n> +               std::optional<float> saturation;\n>         } knobs;\n>  };\n>  \n> @@ -75,11 +76,14 @@ struct IPAFrameContext : public FrameContext {\n>                 int32_t exposure;\n>                 double gain;\n>         } sensor;\n> +\n>         struct {\n>                 double red;\n>                 double blue;\n>         } gains;\n> +\n>         std::optional<double> contrast;\n> +       std::optional<float> saturation;\n>  };\n>  \n>  struct IPAContext {\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 F371CC3200\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 16 May 2025 11:17:30 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 049B868C92;\n\tFri, 16 May 2025 13:17:30 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id AA00368B66\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 16 May 2025 13:17:28 +0200 (CEST)","from pyrite.rasen.tech\n\t(2a01cb09d07af966997fbe763aff4675.ipv6.abo.wanadoo.fr\n\t[IPv6:2a01:cb09:d07a:f966:997f:be76:3aff:4675])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id A55231D8;\n\tFri, 16 May 2025 13:17:10 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"TJnDMK+6\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1747394230;\n\tbh=QZRCsCLDzH3jfBPRDWEF2YWosUqo44gjLP0UyUEiuok=;\n\th=In-Reply-To:References:Subject:From:Cc:To:Date:From;\n\tb=TJnDMK+6Mcg/tDCg7JSicY0veKIhCpDaqLgVdSD4HISrsZVlAsC+no6Si2bkBIZJA\n\tYJcSUSKJcmNi6hYTWTut1o3Io79O6ZM9U2JoUQyvBdvluPgKVerloZR6oNxJn6sLG2\n\tykE3Fr+emEr5Ep4MEXnN0yWEIywCK/peIKPwKQPk=","Content-Type":"text/plain; charset=\"utf-8\"","MIME-Version":"1.0","Content-Transfer-Encoding":"quoted-printable","In-Reply-To":"<20250515160432.119022-2-mzamazal@redhat.com>","References":"<20250515160432.119022-1-mzamazal@redhat.com>\n\t<20250515160432.119022-2-mzamazal@redhat.com>","Subject":"Re: [PATCH v3 1/1] libcamera: software_isp: Add saturation control","From":"Paul Elder <paul.elder@ideasonboard.com>","Cc":"Milan Zamazal <mzamazal@redhat.com>,\n\tLaurent Pinchart <laurent.pinchart@ideasonboard.com>,\n\tKieran Bingham <kieran.bingham@ideasonboard.com>","To":"Milan Zamazal <mzamazal@redhat.com>, libcamera-devel@lists.libcamera.org","Date":"Fri, 16 May 2025 13:17:22 +0200","Message-ID":"<174739424296.530918.16089723990803615032@calcite>","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>"}},{"id":34379,"web_url":"https://patchwork.libcamera.org/comment/34379/","msgid":"<174851700739.896144.10680860279203110050@ping.linuxembedded.co.uk>","date":"2025-05-29T11:10:07","subject":"Re: [PATCH v3 1/1] libcamera: software_isp: Add saturation control","submitter":{"id":4,"url":"https://patchwork.libcamera.org/api/people/4/","name":"Kieran Bingham","email":"kieran.bingham@ideasonboard.com"},"content":"Quoting Paul Elder (2025-05-16 12:17:22)\n> Quoting Milan Zamazal (2025-05-15 18:04:31)\n> > Saturation control is added on top of the colour correction matrix.  A\n> > method of saturation adjustment that can be fully integrated into the\n> > colour correction matrix is used.  The control is available only if Ccm\n> > algorithm is enabled.\n> > \n> > The control uses 0.0-2.0 value range, with 1.0 being unmodified\n> > saturation, 0.0 full desaturation and 2.0 quite saturated.\n> > \n> > The saturation is adjusted by converting to Y'CbCr colour space,\n> > applying the saturation value on the colour axes, and converting back to\n> > RGB.  ITU-R BT.601 conversion is used to convert between the colour\n> > spaces, for no particular reason.\n> > \n> > The colour correction matrix is applied before gamma and the given\n> > matrix is suitable for such a case.  Alternatively, the transformation\n> > used in libcamera rpi ccm.cpp could be used.\n> > \n> > Signed-off-by: Milan Zamazal <mzamazal@redhat.com>\n> > ---\n> >  src/ipa/simple/algorithms/ccm.cpp | 60 +++++++++++++++++++++++++++++--\n> >  src/ipa/simple/algorithms/ccm.h   | 11 ++++++\n> >  src/ipa/simple/ipa_context.h      |  4 +++\n> >  3 files changed, 72 insertions(+), 3 deletions(-)\n> > \n> > diff --git a/src/ipa/simple/algorithms/ccm.cpp b/src/ipa/simple/algorithms/ccm.cpp\n> > index d5ba928d..0a98406c 100644\n> > --- a/src/ipa/simple/algorithms/ccm.cpp\n> > +++ b/src/ipa/simple/algorithms/ccm.cpp\n> > @@ -3,7 +3,7 @@\n> >   * Copyright (C) 2024, Ideas On Board\n> >   * Copyright (C) 2024-2025, Red Hat Inc.\n> >   *\n> > - * Color correction matrix\n> > + * Color correction matrix + saturation\n> >   */\n> >  \n> >  #include \"ccm.h\"\n> > @@ -13,6 +13,8 @@\n> >  \n> >  #include <libcamera/control_ids.h>\n> >  \n> > +#include \"libcamera/internal/matrix.h\"\n> > +\n> >  namespace {\n> >  \n> >  constexpr unsigned int kTemperatureThreshold = 100;\n> > @@ -35,28 +37,77 @@ int Ccm::init([[maybe_unused]] IPAContext &context, const YamlObject &tuningData\n> >         }\n> >  \n> >         context.ccmEnabled = true;\n> > +       context.ctrlMap[&controls::Saturation] = ControlInfo(0.0f, 2.0f, 1.0f);\n> > +\n> > +       return 0;\n> > +}\n> > +\n> > +int Ccm::configure(IPAContext &context,\n> > +                  [[maybe_unused]] const IPAConfigInfo &configInfo)\n> > +{\n> > +       context.activeState.knobs.saturation = std::optional<double>();\n> >  \n> >         return 0;\n> >  }\n> >  \n> > +void Ccm::queueRequest(typename Module::Context &context,\n> > +                      [[maybe_unused]] const uint32_t frame,\n> > +                      [[maybe_unused]] typename Module::FrameContext &frameContext,\n> > +                      const ControlList &controls)\n> > +{\n> > +       const auto &saturation = controls.get(controls::Saturation);\n> > +       if (saturation.has_value()) {\n> > +               context.activeState.knobs.saturation = saturation;\n> > +               LOG(IPASoftCcm, Debug) << \"Setting saturation to \" << saturation.value();\n> > +       }\n> > +}\n> > +\n> > +void Ccm::applySaturation(Matrix<float, 3, 3> &ccm, float saturation)\n> > +{\n> > +       /* https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion */\n> > +       const Matrix<float, 3, 3> rgb2ycbcr{\n> > +               { 0.256788235294, 0.504129411765, 0.0979058823529,\n> > +                 -0.148223529412, -0.290992156863, 0.439215686275,\n> > +                 0.439215686275, -0.367788235294, -0.0714274509804 }\n> > +       };\n> > +       const Matrix<float, 3, 3> ycbcr2rgb{\n> > +               { 1.16438356164, 0, 1.59602678571,\n> > +                 1.16438356164, -0.391762290094, -0.812967647235,\n> > +                 1.16438356164, 2.01723214285, 0 }\n> > +       };\n> > +       const Matrix<float, 3, 3> saturationMatrix{\n> > +               { 1, 0, 0,\n> > +                 0, saturation, 0,\n> > +                 0, 0, saturation }\n> > +       };\n> > +       ccm = ycbcr2rgb * saturationMatrix * rgb2ycbcr * ccm;\n> > +}\n> > +\n> >  void Ccm::prepare(IPAContext &context, const uint32_t frame,\n> >                   IPAFrameContext &frameContext, [[maybe_unused]] DebayerParams *params)\n> >  {\n> > +       auto &saturation = context.activeState.knobs.saturation;\n> > +\n> >         const unsigned int ct = context.activeState.awb.temperatureK;\n> >  \n> > -       /* Change CCM only on bigger temperature changes. */\n> > +       /* Change CCM only on saturation or bigger temperature changes. */\n> >         if (frame > 0 &&\n> > -           utils::abs_diff(ct, lastCt_) < kTemperatureThreshold) {\n> > +           utils::abs_diff(ct, lastCt_) < kTemperatureThreshold &&\n> > +           saturation == lastSaturation_) {\n> \n> Hm, at first I thought that maybe it's not a good idea to use the equivalence\n> operator for floats, but I suppose that even if the value that the user submits\n> gets rounded due to precision, it would still result in the same value very\n> time (if they submit the same value). I don't imagine that this equivalence\n> would cause a significant increase in unnecessary ccm operations so maybe it's\n> fine.\n> \n> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>\n\nI wonder how we might break this up with GPU ISP sometime ... but I\nthink this is fine now, and provides the abiltiy to get more visual\nfeedback of control handling.\n\n\nReviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>\n\n> \n> >                 frameContext.ccm.ccm = context.activeState.ccm.ccm;\n> >                 context.activeState.ccm.changed = false;\n> >                 return;\n> >         }\n> >  \n> >         lastCt_ = ct;\n> > +       lastSaturation_ = saturation;\n> >         Matrix<float, 3, 3> ccm = ccm_.getInterpolated(ct);\n> > +       if (saturation)\n> > +               applySaturation(ccm, saturation.value());\n> >  \n> >         context.activeState.ccm.ccm = ccm;\n> >         frameContext.ccm.ccm = ccm;\n> > +       frameContext.saturation = saturation;\n> >         context.activeState.ccm.changed = true;\n> >  }\n> >  \n> > @@ -67,6 +118,9 @@ void Ccm::process([[maybe_unused]] IPAContext &context,\n> >                   ControlList &metadata)\n> >  {\n> >         metadata.set(controls::ColourCorrectionMatrix, frameContext.ccm.ccm.data());\n> > +\n> > +       const auto &saturation = frameContext.saturation;\n> > +       metadata.set(controls::Saturation, saturation.value_or(1.0));\n> >  }\n> >  \n> >  REGISTER_IPA_ALGORITHM(Ccm, \"Ccm\")\n> > diff --git a/src/ipa/simple/algorithms/ccm.h b/src/ipa/simple/algorithms/ccm.h\n> > index f4e2b85b..8279a3d5 100644\n> > --- a/src/ipa/simple/algorithms/ccm.h\n> > +++ b/src/ipa/simple/algorithms/ccm.h\n> > @@ -7,6 +7,8 @@\n> >  \n> >  #pragma once\n> >  \n> > +#include <optional>\n> > +\n> >  #include \"libcamera/internal/matrix.h\"\n> >  \n> >  #include <libipa/interpolator.h>\n> > @@ -24,6 +26,12 @@ public:\n> >         ~Ccm() = default;\n> >  \n> >         int init(IPAContext &context, const YamlObject &tuningData) override;\n> > +       int configure(IPAContext &context,\n> > +                     const IPAConfigInfo &configInfo) override;\n> > +       void queueRequest(typename Module::Context &context,\n> > +                         const uint32_t frame,\n> > +                         typename Module::FrameContext &frameContext,\n> > +                         const ControlList &controls) override;\n> >         void prepare(IPAContext &context,\n> >                      const uint32_t frame,\n> >                      IPAFrameContext &frameContext,\n> > @@ -34,7 +42,10 @@ public:\n> >                      ControlList &metadata) override;\n> >  \n> >  private:\n> > +       void applySaturation(Matrix<float, 3, 3> &ccm, float saturation);\n> > +\n> >         unsigned int lastCt_;\n> > +       std::optional<float> lastSaturation_;\n> >         Interpolator<Matrix<float, 3, 3>> ccm_;\n> >  };\n> >  \n> > diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h\n> > index 88cc6c35..a471b80a 100644\n> > --- a/src/ipa/simple/ipa_context.h\n> > +++ b/src/ipa/simple/ipa_context.h\n> > @@ -63,6 +63,7 @@ struct IPAActiveState {\n> >         struct {\n> >                 /* 0..2 range, 1.0 = normal */\n> >                 std::optional<double> contrast;\n> > +               std::optional<float> saturation;\n> >         } knobs;\n> >  };\n> >  \n> > @@ -75,11 +76,14 @@ struct IPAFrameContext : public FrameContext {\n> >                 int32_t exposure;\n> >                 double gain;\n> >         } sensor;\n> > +\n> >         struct {\n> >                 double red;\n> >                 double blue;\n> >         } gains;\n> > +\n> >         std::optional<double> contrast;\n> > +       std::optional<float> saturation;\n> >  };\n> >  \n> >  struct IPAContext {\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 75926C31E9\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu, 29 May 2025 11:10:14 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 16C3068DA6;\n\tThu, 29 May 2025 13:10:13 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 98D56614CE\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 29 May 2025 13:10:10 +0200 (CEST)","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 5D4C7778;\n\tThu, 29 May 2025 13:09:43 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"Z96KFmrE\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1748516983;\n\tbh=QPiMYMVjeXaqSXK7tx9IRiNvKGj2zbj5YB6eeqtEiqA=;\n\th=In-Reply-To:References:Subject:From:Cc:To:Date:From;\n\tb=Z96KFmrEt9IrYVE84ORGGisGvqwMdStYcvZiaGUMeDt0/7n6BxfZz/V5+TMZ3IZBR\n\tIVah5t9nRa5s9a8c9XAooP9qv1URIOgI9hUXCt3NBU5RWbf1Gv9qyoseGr6+nCuiUq\n\tPOchJYfvs92Q79jBshS34y04BfU8kpYKj6KVV8W8=","Content-Type":"text/plain; charset=\"utf-8\"","MIME-Version":"1.0","Content-Transfer-Encoding":"quoted-printable","In-Reply-To":"<174739424296.530918.16089723990803615032@calcite>","References":"<20250515160432.119022-1-mzamazal@redhat.com>\n\t<20250515160432.119022-2-mzamazal@redhat.com>\n\t<174739424296.530918.16089723990803615032@calcite>","Subject":"Re: [PATCH v3 1/1] libcamera: software_isp: Add saturation control","From":"Kieran Bingham <kieran.bingham@ideasonboard.com>","Cc":"Milan Zamazal <mzamazal@redhat.com>,\n\tLaurent Pinchart <laurent.pinchart@ideasonboard.com>","To":"Milan Zamazal <mzamazal@redhat.com>,\n\tPaul Elder <paul.elder@ideasonboard.com>,\n\tlibcamera-devel@lists.libcamera.org","Date":"Thu, 29 May 2025 12:10:07 +0100","Message-ID":"<174851700739.896144.10680860279203110050@ping.linuxembedded.co.uk>","User-Agent":"alot/0.9.1","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>"}}]