[{"id":35907,"web_url":"https://patchwork.libcamera.org/comment/35907/","msgid":"<175822087394.3990114.6026778704604652527@ping.linuxembedded.co.uk>","date":"2025-09-18T18:41:13","subject":"Re: [PATCH v4 18/19] ipa: rkisp1: Add WDR algorithm","submitter":{"id":4,"url":"https://patchwork.libcamera.org/api/people/4/","name":"Kieran Bingham","email":"kieran.bingham@ideasonboard.com"},"content":"Quoting Stefan Klug (2025-09-18 15:43:27)\n> Add a WDR algorithm to do global tone mapping. Global tone mapping is\n> used to increase the perceived dynamic range of an image. The typical\n> effect is that in areas that are normally overexposed, additional\n> structure becomes visible.\n> \n> The overall idea is that the algorithm applies an exposure value\n> correction to underexpose the image to the point where only a small\n> number of saturated pixels is left. This artificial underexposure is\n> then mitigated by applying a tone mapping curve.\n> \n> This algorithm implements 4 tone mapping strategies:\n> - Linear\n> - Power\n> - Exponential\n> - Histogram equalization\n> \n> Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>\n> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>\n> \n> ---\n> \n> Changes in v4:\n> - Properly initialize activeState.wdr.gain\n> - Fixed some typos and improved wording\n> \n> Changes in v3:\n> - Removed the need for a separate regulation loop, by not relying on an\n>   added ExposureValue but by applying a constraint to AGC regulation and\n> deducing the required WDR gain from the histogram. This makes the\n> structure easier and hopefully less prone to oscillations.\n> - Dropped debug metadata as this needs more discussions and is not\n>   required for the algorithm to work.\n> - Dropped minExposureValue as it is not used anymore.\n> - Added a damping on the gain applied by the WDR curve\n> - Moved strength into activeState so that it is reset on configure()\n> \n> Changes in v2:\n> - Fixed default value for min bright pixels\n> - Added check for supported params type\n> - Reset PID controller\n> - Various fixes from Pauls review\n> ---\n>  src/ipa/rkisp1/algorithms/agc.cpp     |   7 +-\n>  src/ipa/rkisp1/algorithms/meson.build |   1 +\n>  src/ipa/rkisp1/algorithms/wdr.cpp     | 493 ++++++++++++++++++++++++++\n>  src/ipa/rkisp1/algorithms/wdr.h       |  58 +++\n>  src/ipa/rkisp1/ipa_context.h          |  14 +\n>  src/ipa/rkisp1/params.cpp             |   1 +\n>  src/ipa/rkisp1/params.h               |   2 +\n>  src/libcamera/control_ids_draft.yaml  |  62 ++++\n>  8 files changed, 637 insertions(+), 1 deletion(-)\n>  create mode 100644 src/ipa/rkisp1/algorithms/wdr.cpp\n>  create mode 100644 src/ipa/rkisp1/algorithms/wdr.h\n> \n> diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp\n> index 046a1ac9caa2..f7ea4c70e2ae 100644\n> --- a/src/ipa/rkisp1/algorithms/agc.cpp\n> +++ b/src/ipa/rkisp1/algorithms/agc.cpp\n> @@ -594,7 +594,12 @@ void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,\n>                 maxAnalogueGain = frameContext.agc.gain;\n>         }\n>  \n> -       setLimits(minExposureTime, maxExposureTime, minAnalogueGain, maxAnalogueGain, {});\n> +       std::vector<AgcMeanLuminance::AgcConstraint> additionalConstraints;\n> +       if (context.activeState.wdr.mode != controls::draft::WdrOff)\n> +               additionalConstraints.push_back(context.activeState.wdr.constraint);\n> +\n> +       setLimits(minExposureTime, maxExposureTime, minAnalogueGain, maxAnalogueGain,\n> +                 std::move(additionalConstraints));\n>  \n>         /*\n>          * The Agc algorithm needs to know the effective exposure value that was\n> diff --git a/src/ipa/rkisp1/algorithms/meson.build b/src/ipa/rkisp1/algorithms/meson.build\n> index 2e42a80cf99d..d329dbfb432d 100644\n> --- a/src/ipa/rkisp1/algorithms/meson.build\n> +++ b/src/ipa/rkisp1/algorithms/meson.build\n> @@ -14,4 +14,5 @@ rkisp1_ipa_algorithms = files([\n>      'gsl.cpp',\n>      'lsc.cpp',\n>      'lux.cpp',\n> +    'wdr.cpp',\n>  ])\n> diff --git a/src/ipa/rkisp1/algorithms/wdr.cpp b/src/ipa/rkisp1/algorithms/wdr.cpp\n> new file mode 100644\n> index 000000000000..a4acf094388f\n> --- /dev/null\n> +++ b/src/ipa/rkisp1/algorithms/wdr.cpp\n> @@ -0,0 +1,493 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2025, Ideas On Board\n> + *\n> + * RkISP1 Wide Dynamic Range control\n> + */\n> +\n> +#include \"wdr.h\"\n> +\n> +#include <libcamera/base/log.h>\n> +#include <libcamera/base/utils.h>\n> +\n> +#include \"libcamera/internal/yaml_parser.h\"\n> +\n> +#include <libipa/agc_mean_luminance.h>\n> +#include <libipa/histogram.h>\n> +#include <libipa/pwl.h>\n> +\n> +#include \"linux/rkisp1-config.h\"\n> +\n> +/**\n> + * \\file wdr.h\n> + */\n> +\n> +namespace libcamera {\n> +\n> +namespace ipa::rkisp1::algorithms {\n> +\n> +/**\n> + * \\class WideDynamicRange\n> + * \\brief RkISP1 Wide Dynamic Range algorithm\n> + *\n> + * This algorithm implements automatic global tone mapping for the RkISP1.\n> + * Global tone mapping is done by the GWDR hardware block and applies\n> + * a global tone mapping curve to the image to increase the perceived dynamic\n> + * range. Imagine an indoor scene with bright outside visible through the\n> + * windows. With normal exposure settings, the windows will be completely\n> + * saturated and no structure (sky/clouds) will be visible because the AEGC has\n> + * to increase overall exposure to reach a certain level of mean brightness. In\n> + * WDR mode, the algorithm will artifically reduce the exposure time so that the\n> + * texture and colours become visible in the formerly saturated areas. Then the\n> + * global tone mapping curve is applied to mitigate the loss of brightness.\n> + *\n> + * Calculating that tone mapping curve is the most difficult part. This\n> + * algorithm implements four tone mapping strategies:\n> + * - Linear: The tone mapping curve is a combination of two linear functions\n> + *   with one kneepoint\n> + * - Power: The tone mapping curve follows a power function\n> + * - Exponential: The tone mapping curve follows an exponential function\n> + * - HistogramEqualization: The tone mapping curve tries to equalize the\n> + *   histogram\n> + *\n> + * The overall strategy is the same in all cases: Add a constraint to the AEGC\n> + * regulation so that the number of nearly saturated pixels goes below a given\n> + * threshold (default 2%). This threshold can either be specified in the tuning\n> + * file or set via the WdrMaxBrightPixels control.\n> + *\n> + * The global tone mapping curve is then calculated so that it accounts for the\n> + * reduction of brightness due to the exposure constraint. We'll call this the\n> + * WDR-gain. As the result of tone mapping is very difficult to quantize and is\n> + * by definition a lossy process there is not a single \"correct\" solution on how\n> + * this curve should look like.\n> + *\n> + * The approach taken here is based on a simple linear model. Consider a pixel\n> + * that was originally 50% grey. It will have its exposure pushed down by the\n> + * WDR's initial exposure compensation. This value then needs to be pushed back\n> + * up by the tone mapping curve so that it is 50% grey again. This point serves\n> + * as our kneepoint. To get to this kneepoint, this pixel and all darker pixels\n> + * (to the left of the kneepoint on the tone mapping curve) will simply have the\n> + * exposure compensation undone by WDR-gain. This cancels out the\n> + * original exposure compensation, which was 1/WDR-gain. The remaining\n> + * brigher pixels (to the right of the kneepoint on the tone mapping curve) will\n> + * be compressed. The WdrStrength control adjusts the gain of the left part of\n> + * the tone mapping curve.\n> + *\n> + * In the Power and Exponential modes, the curves are calculated so that they\n> + * pass through that kneepoint.\n> + *\n> + * The histogram equalization mode tries to equalize the histogram of the\n> + * image and acts independently of the calculated exposure value.\n> + *\n> + * \\code{.unparsed}\n> + * algorithms:\n> + *   - WideDynamicRange:\n> + *       ExposureConstraint:\n> + *         MaxBrightPixels: 0.02\n> + *         yTarget: 0.95\n> + * \\endcode\n> + */\n> +\n> +LOG_DEFINE_CATEGORY(RkISP1Wdr)\n> +\n> +static constexpr unsigned int kTonecurveXIntervals = RKISP1_CIF_ISP_WDR_CURVE_NUM_INTERV;\n> +\n> +/*\n> + * Increasing interval sizes. The intervals are crafted so that they sum\n> + * up to 4096. This results in better fitting curves than the constant intervals\n> + * (all entries are 4)\n> + */\n> +static constexpr std::array<int, kTonecurveXIntervals> kLoglikeIntervals = {\n> +       { 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4,\n> +         4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6 }\n> +};\n> +\n> +WideDynamicRange::WideDynamicRange()\n> +{\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::init\n> + */\n> +int WideDynamicRange::init([[maybe_unused]] IPAContext &context,\n> +                          [[maybe_unused]] const YamlObject &tuningData)\n> +{\n> +       if (!(context.hw.supportedBlocks & 1 << RKISP1_EXT_PARAMS_BLOCK_TYPE_WDR)) {\n> +               LOG(RkISP1Wdr, Error)\n> +                       << \"Wide Dynamic Range not supported by the hardware or kernel.\";\n> +               return -ENOTSUP;\n> +       }\n> +\n> +       toneCurveIntervalValues_ = kLoglikeIntervals;\n> +\n> +       /* Calculate a list of normed x values */\n> +       toneCurveX_[0] = 0.0;\n> +       int lastValue = 0;\n> +       for (unsigned int i = 1; i < toneCurveX_.size(); i++) {\n> +               lastValue += std::pow(2, toneCurveIntervalValues_[i - 1] + 3);\n> +               lastValue = std::min(lastValue, 4096);\n> +               toneCurveX_[i] = lastValue / 4096.0;\n> +       }\n> +\n> +       exposureConstraintMaxBrightPixels_ = 0.02;\n> +       exposureConstraintY_ = 0.95;\n> +\n> +       const auto &constraint = tuningData[\"ExposureConstraint\"];\n> +       if (!constraint.isDictionary()) {\n> +               LOG(RkISP1Wdr, Warning)\n> +                       << \"ExposureConstraint not found in tuning data.\"\n> +                          \"Using default values MaxBrightPixels: \"\n> +                       << exposureConstraintMaxBrightPixels_\n> +                       << \" yTarget: \" << exposureConstraintY_;\n> +       } else {\n> +               exposureConstraintMaxBrightPixels_ =\n> +                       constraint[\"MaxBrightPixels\"]\n> +                               .get<double>()\n> +                               .value_or(exposureConstraintMaxBrightPixels_);\n> +               exposureConstraintY_ =\n> +                       constraint[\"yTarget\"]\n> +                               .get<double>()\n> +                               .value_or(exposureConstraintY_);\n> +       }\n> +\n> +       context.ctrlMap[&controls::draft::WdrMode] =\n> +               ControlInfo(controls::draft::WdrModeValues, controls::draft::WdrOff);\n> +       context.ctrlMap[&controls::draft::WdrStrength] =\n> +               ControlInfo(0.0f, 2.0f, 1.0f);\n> +       context.ctrlMap[&controls::draft::WdrMaxBrightPixels] =\n> +               ControlInfo(0.0f, 1.0f, static_cast<float>(exposureConstraintMaxBrightPixels_));\n> +\n> +       applyCompensationLinear(1.0, 0.0);\n> +\n> +       return 0;\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::configure\n> + */\n> +int WideDynamicRange::configure(IPAContext &context,\n> +                               [[maybe_unused]] const IPACameraSensorInfo &configInfo)\n> +{\n> +       context.activeState.wdr.mode = controls::draft::WdrOff;\n> +       context.activeState.wdr.gain = 1.0;\n> +       context.activeState.wdr.strength = 1.0;\n> +       auto &constraint = context.activeState.wdr.constraint;\n> +       constraint.bound = AgcMeanLuminance::AgcConstraint::Bound::Upper;\n> +       constraint.qHi = 1.0;\n> +       constraint.qLo = 1.0 - exposureConstraintMaxBrightPixels_;\n> +       constraint.yTarget = exposureConstraintY_;\n> +       return 0;\n> +}\n> +\n> +void WideDynamicRange::applyHistogramEqualization(double strength)\n> +{\n> +       if (hist_.empty())\n> +               return;\n> +\n> +       /**\n\nWhen doxygen builds this - does it make sense to have this as a\ndoxygen block ?\n\n> +        * Apply a factor on strength, so that it roughly matches the optical\n> +        * impression that is produced by the other algorithms. The goal is that\n> +        * the user can switch algorithms for different looks but similar\n> +        * \"strength\".\n> +        */\n> +       strength *= 0.65;\n> +\n> +       /**\n\nSame.\n\n> +        * In a fully equalized histogram, all bins have the same value. Try\n> +        * to equalize the histogram by applying a gain or damping depending on\n> +        * the distance of the actual bin value from that norm.\n> +        */\n> +       std::vector<double> gains;\n> +       gains.resize(hist_.size());\n> +       double sum = 0;\n> +       double norm = 1.0 / (gains.size());\n> +       for (unsigned i = 0; i < hist_.size(); i++) {\n> +               double diff = 1.0 + strength * (hist_[i] - norm) / norm;\n> +               gains[i] = diff;\n> +               sum += diff;\n> +       }\n> +\n> +       /* Never amplify the last entry. */\n> +       gains.back() = std::max(gains.back(), 1.0);\n> +\n> +       double scale = gains.size() / sum;\n> +       for (auto &v : gains)\n> +               v *= scale;\n> +\n> +       Pwl pwl;\n> +       double step = 1.0 / gains.size();\n> +       double lastX = 0;\n> +       double lastY = 0;\n> +\n> +       pwl.append(lastX, lastY);\n> +       for (unsigned int i = 0; i < gains.size() - 1; i++) {\n> +               lastY += gains[i] * step;\n> +               lastX += step;\n> +               pwl.append(lastX, lastY);\n> +       }\n> +       pwl.append(1.0, 1.0);\n> +\n> +       for (unsigned int i = 0; i < toneCurveX_.size(); i++)\n> +               toneCurveY_[i] = pwl.eval(toneCurveX_[i]);\n> +}\n> +\n> +Vector<double, 2> WideDynamicRange::kneePoint(double gain, double strength)\n> +{\n> +       gain = std::pow(gain, strength);\n> +       double y = 0.5;\n> +       double x = y / gain;\n> +\n> +       return { { x, y } };\n> +}\n> +\n> +void WideDynamicRange::applyCompensationLinear(double gain, double strength)\n> +{\n> +       auto kp = kneePoint(gain, strength);\n> +       double g1 = kp.y() / kp.x();\n> +       double g2 = (kp.y() - 1) / (kp.x() - 1);\n> +\n> +       for (unsigned int i = 0; i < toneCurveX_.size(); i++) {\n> +               double x = toneCurveX_[i];\n> +               double y;\n> +               if (x <= kp.x()) {\n> +                       y = g1 * x;\n> +               } else {\n> +                       y = g2 * x + 1 - g2;\n> +               }\n> +               toneCurveY_[i] = y;\n> +       }\n> +}\n> +\n> +void WideDynamicRange::applyCompensationPower(double gain, double strength)\n> +{\n> +       double e = 1.0;\n> +       if (strength > 1e-6) {\n> +               auto kp = kneePoint(gain, strength);\n> +               /* Calculate an exponent to go through the knee point. */\n> +               e = log(kp.y()) / log(kp.x());\n> +       }\n> +\n> +       /**\n\nSame ...\n\nBut I do love these documentation blocks. I wonder if the 'IPA' docs\nwould almost build into a dedicated page somehow with all of this\ncontent in the future.\n\n\n> +        * The power function tends to be extremely steep at the beginning. This\n> +        * leads to noise and image artifacts in the dark areas. To mitigate\n> +        * that, we add a short linear section at the beginning of the curve.\n> +        * The connection between linear and power is the point where the linear\n> +        * section reaches the y level yLin. The power curve is then scaled so\n> +        * that it starts at the connection point with the steepness it would\n> +        * have at y=yLin but still goes through 1,1\n> +        **/\n\nI think even doxygen uses a single * at the end ?\n\n> +       double yLin = 0.1;\n> +       /* x position of the connection point */\n> +       double xb = yLin / gain;\n> +       /* x offset for the scaled power function */\n> +       double q = xb - std::exp(std::log(yLin) / e);\n> +\n> +       for (unsigned int i = 0; i < toneCurveX_.size(); i++) {\n> +               double x = toneCurveX_[i];\n> +               if (x < xb) {\n> +                       toneCurveY_[i] = x * gain;\n> +               } else {\n> +                       x = (x - q) / (1 - q);\n> +                       toneCurveY_[i] = std::pow(x, e);\n> +               }\n> +       }\n> +}\n> +\n> +void WideDynamicRange::applyCompensationExponential(double gain, double strength)\n> +{\n> +       double k = 0.1;\n> +       auto kp = kneePoint(gain, strength);\n> +       double kx = kp.x();\n> +       double ky = kp.y();\n> +\n> +       if (kx > ky) {\n> +               LOG(RkISP1Wdr, Warning) << \"Invalid knee point: \" << kp;\n> +               kx = ky;\n> +       }\n> +\n> +       /*\n> +        * The exponential curve is based on the function proposed by Glozman\n> +        * et al. in\n> +        * S. Glozman, T. Kats, and O. Yadid-Pecht, \"Exponent Operator Based\n> +        * Tone Mapping Algorithm for Color Wide Dynamic Range Images,\" 2011.\n> +        *\n> +        * That function uses a k factor as parameter for the WDR compression\n> +        * curve:\n> +        * k=0: maximum compression\n> +        * k=infinity: linear curve\n> +        *\n> +        * To calculate a k factor that results in a curve that passes through\n> +        * the kneepoint, the equation needs to be solved for k after inserting\n> +        * the kneepoint.  This can be formulated as search for a zero point.\n> +        * Unfortunately there is no closed solution for that transformation.\n> +        * Using newton's method to approximate the value is numerically\n> +        * unstable.\n> +        *\n> +        * Luckily the function only crosses the x axis once and for the set of\n> +        * possible kneepoints, a negative and a positive point can be guessed.\n> +        * The approximation is then implemented using bisection.\n> +        */\n> +       if (std::abs(kx - ky) < 0.001) {\n> +               k = 1e8;\n> +       } else {\n> +               double kl = 0.0001;\n> +               double kh = 1000;\n> +\n> +               auto fk = [=](double v) {\n> +                       return std::exp(-kx / v) -\n> +                              ky * std::exp(-1.0 / v) + ky - 1.0;\n> +               };\n> +\n> +               ASSERT(fk(kl) < 0);\n> +               ASSERT(fk(kh) > 0);\n> +\n> +               k = kh / 10.0;\n> +               while (fk(k) > 0) {\n> +                       kh = k;\n> +                       k /= 10.0;\n> +               }\n> +\n> +               do {\n> +                       k = (kl + kh) / 2;\n> +                       if (fk(k) < 0)\n> +                               kl = k;\n> +                       else\n> +                               kh = k;\n> +               } while (std::abs(kh - kl) > 1e-3);\n> +       }\n> +\n> +       double a = 1.0 / (1.0 - std::exp(-1.0 / k));\n> +       for (unsigned int i = 0; i < toneCurveX_.size(); i++)\n> +               toneCurveY_[i] = a * (1.0 - std::exp(-toneCurveX_[i] / k));\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::queueRequest\n> + */\n> +void WideDynamicRange::queueRequest([[maybe_unused]] IPAContext &context,\n> +                                   [[maybe_unused]] const uint32_t frame,\n> +                                   IPAFrameContext &frameContext,\n> +                                   const ControlList &controls)\n> +{\n> +       auto &activeState = context.activeState;\n> +\n> +       const auto &mode = controls.get(controls::draft::WdrMode);\n> +       if (mode)\n> +               activeState.wdr.mode = static_cast<controls::draft::WdrModeEnum>(*mode);\n> +\n> +       const auto &brightPixels = controls.get(controls::draft::WdrMaxBrightPixels);\n> +       if (brightPixels)\n> +               activeState.wdr.constraint.qLo = 1.0 - *brightPixels;\n> +\n> +       const auto &strength = controls.get(controls::draft::WdrStrength);\n> +       if (strength)\n> +               activeState.wdr.strength = *strength;\n> +\n> +       frameContext.wdr.mode = activeState.wdr.mode;\n> +       frameContext.wdr.strength = activeState.wdr.strength;\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::prepare\n> + */\n> +void WideDynamicRange::prepare(IPAContext &context,\n> +                              [[maybe_unused]] const uint32_t frame,\n> +                              IPAFrameContext &frameContext,\n> +                              RkISP1Params *params)\n> +{\n> +       if (!params) {\n> +               LOG(RkISP1Wdr, Warning) << \"Params is null\";\n> +               return;\n> +       }\n> +\n> +       auto mode = frameContext.wdr.mode;\n> +\n> +       auto config = params->block<BlockType::Wdr>();\n> +       config.setEnabled(mode != controls::draft::WdrOff);\n> +\n> +       /* Calculate how much EV we need to compensate with the WDR curve. */\n> +       double gain = context.activeState.wdr.gain;\n> +       frameContext.wdr.gain = gain;\n> +\n> +       if (mode == controls::draft::WdrOff) {\n> +               applyCompensationLinear(1.0, 0.0);\n> +       } else if (mode == controls::draft::WdrLinear) {\n> +               applyCompensationLinear(gain, frameContext.wdr.strength);\n> +       } else if (mode == controls::draft::WdrPower) {\n> +               applyCompensationPower(gain, frameContext.wdr.strength);\n> +       } else if (mode == controls::draft::WdrExponential) {\n> +               applyCompensationExponential(gain, frameContext.wdr.strength);\n> +       } else if (mode == controls::draft::WdrHistogramEqualization) {\n> +               applyHistogramEqualization(frameContext.wdr.strength);\n> +       }\n> +\n> +       /* Reset value */\n> +       config->dmin_strength = 0x10;\n> +       config->dmin_thresh = 0;\n> +\n> +       for (unsigned int i = 0; i < kTonecurveXIntervals; i++) {\n> +               int v = toneCurveIntervalValues_[i];\n> +               config->tone_curve.dY[i / 8] |= (v & 0x07) << ((i % 8) * 4);\n> +       }\n> +\n> +       /*\n> +        * Fix the curve to adhere to the hardware constraints. Don't apply a\n> +        * constraint on the first element, which is most likely zero anyways.\n> +        */\n> +       int lastY = toneCurveY_[0] * 4096.0;\n> +       for (unsigned int i = 0; i < toneCurveX_.size(); i++) {\n> +               int diff = static_cast<int>(toneCurveY_[i] * 4096.0) - lastY;\n> +               diff = std::clamp(diff, -2048, 2048);\n> +               lastY = lastY + diff;\n> +               config->tone_curve.ym[i] = lastY;\n> +       }\n> +}\n> +\n> +void WideDynamicRange::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,\n> +                              IPAFrameContext &frameContext,\n> +                              const rkisp1_stat_buffer *stats,\n> +                              ControlList &metadata)\n> +{\n> +       if (!stats || !(stats->meas_type & RKISP1_CIF_ISP_STAT_HIST)) {\n> +               LOG(RkISP1Wdr, Warning) << \"No histogram data in statistics\";\n> +               return;\n> +       }\n> +\n> +       const rkisp1_cif_isp_stat *params = &stats->params;\n> +       auto mode = frameContext.wdr.mode;\n> +\n> +       metadata.set(controls::draft::WdrMode, mode);\n> +\n> +       Histogram cumHist({ params->hist.hist_bins, context.hw.numHistogramBins },\n> +                         [](uint32_t x) { return x >> 4; });\n> +\n> +       /* Calculate the gain needed to reach the requested yTarget*/\n\nspace between yTarget and */\n\n> +       double value = cumHist.interQuantileMean(0, 1.0) / cumHist.bins();\n> +       double gain = context.activeState.agc.automatic.yTarget / value;\n> +       gain = std::max(gain, 1.0);\n> +\n> +       double speed = 0.2;\n> +       gain = gain * speed + context.activeState.wdr.gain * (1.0 - speed);\n> +\n> +       context.activeState.wdr.gain = gain;\n> +\n> +       std::vector<double> hist;\n> +       double sum = 0;\n> +       for (unsigned i = 0; i < context.hw.numHistogramBins; i++) {\n> +               double v = params->hist.hist_bins[i] >> 4;\n> +               hist.push_back(v);\n> +               sum += v;\n> +       }\n> +\n> +       /* Scale so that the entries sum up to 1. */\n> +       double scale = 1.0 / sum;\n> +       for (auto &v : hist)\n> +               v *= scale;\n> +       hist_.swap(hist);\n> +}\n> +\n> +REGISTER_IPA_ALGORITHM(WideDynamicRange, \"WideDynamicRange\")\n> +\n> +} /* namespace ipa::rkisp1::algorithms */\n> +\n> +} /* namespace libcamera */\n> diff --git a/src/ipa/rkisp1/algorithms/wdr.h b/src/ipa/rkisp1/algorithms/wdr.h\n> new file mode 100644\n> index 000000000000..90548ce9e840\n> --- /dev/null\n> +++ b/src/ipa/rkisp1/algorithms/wdr.h\n> @@ -0,0 +1,58 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2021-2022, Ideas On Board\n\nPerhaps a newer date here.\n\n> + *\n> + * RkISP1 Wide Dynamic Range control\n> + */\n> +\n> +#pragma once\n> +\n> +#include <libcamera/control_ids.h>\n> +\n> +#include \"linux/rkisp1-config.h\"\n> +\n> +#include \"algorithm.h\"\n> +\n> +namespace libcamera {\n> +\n> +namespace ipa::rkisp1::algorithms {\n> +\n> +class WideDynamicRange : public Algorithm\n> +{\n> +public:\n> +       WideDynamicRange();\n> +       ~WideDynamicRange() = default;\n> +\n> +       int init(IPAContext &context, const YamlObject &tuningData) override;\n> +       int configure(IPAContext &context, const IPACameraSensorInfo &configInfo) override;\n> +\n> +       void queueRequest(IPAContext &context, const uint32_t frame,\n> +                         IPAFrameContext &frameContext,\n> +                         const ControlList &controls) override;\n> +       void prepare(IPAContext &context, const uint32_t frame,\n> +                    IPAFrameContext &frameContext,\n> +                    RkISP1Params *params) override;\n> +       void process(IPAContext &context, const uint32_t frame,\n> +                    IPAFrameContext &frameContext,\n> +                    const rkisp1_stat_buffer *stats,\n> +                    ControlList &metadata) override;\n> +\n> +private:\n> +       Vector<double, 2> kneePoint(double gain, double strength);\n> +       void applyCompensationLinear(double gain, double strength);\n> +       void applyCompensationPower(double gain, double strength);\n> +       void applyCompensationExponential(double gain, double strength);\n> +       void applyHistogramEqualization(double strength);\n> +\n> +       double exposureConstraintMaxBrightPixels_;\n> +       double exposureConstraintY_;\n> +\n> +       std::vector<double> hist_;\n> +\n> +       std::array<int, RKISP1_CIF_ISP_WDR_CURVE_NUM_INTERV> toneCurveIntervalValues_;\n> +       std::array<double, RKISP1_CIF_ISP_WDR_CURVE_NUM_INTERV + 1> toneCurveX_;\n> +       std::array<double, RKISP1_CIF_ISP_WDR_CURVE_NUM_INTERV + 1> toneCurveY_;\n> +};\n> +\n> +} /* namespace ipa::rkisp1::algorithms */\n> +} /* namespace libcamera */\n> diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h\n> index 113b90428008..a662e37c4079 100644\n> --- a/src/ipa/rkisp1/ipa_context.h\n> +++ b/src/ipa/rkisp1/ipa_context.h\n> @@ -26,6 +26,7 @@\n>  \n>  #include <libipa/camera_sensor_helper.h>\n>  #include <libipa/fc_queue.h>\n> +#include \"libipa/agc_mean_luminance.h\"\n>  \n>  namespace libcamera {\n>  \n> @@ -131,6 +132,13 @@ struct IPAActiveState {\n>         struct {\n>                 double gamma;\n>         } goc;\n> +\n> +       struct {\n> +               controls::draft::WdrModeEnum mode;\n> +               AgcMeanLuminance::AgcConstraint constraint;\n> +               double gain;\n> +               double strength;\n> +       } wdr;\n>  };\n>  \n>  struct IPAFrameContext : public FrameContext {\n> @@ -200,6 +208,12 @@ struct IPAFrameContext : public FrameContext {\n>         struct {\n>                 double lux;\n>         } lux;\n> +\n> +       struct {\n> +               controls::draft::WdrModeEnum mode;\n> +               double strength;\n> +               double gain;\n> +       } wdr;\n>  };\n>  \n>  struct IPAContext {\n> diff --git a/src/ipa/rkisp1/params.cpp b/src/ipa/rkisp1/params.cpp\n> index 4c0b051ce65d..5edb36c91b87 100644\n> --- a/src/ipa/rkisp1/params.cpp\n> +++ b/src/ipa/rkisp1/params.cpp\n> @@ -74,6 +74,7 @@ const std::map<BlockType, BlockTypeInfo> kBlockTypeInfo = {\n>         RKISP1_BLOCK_TYPE_ENTRY_EXT(CompandBls, COMPAND_BLS, compand_bls),\n>         RKISP1_BLOCK_TYPE_ENTRY_EXT(CompandExpand, COMPAND_EXPAND, compand_curve),\n>         RKISP1_BLOCK_TYPE_ENTRY_EXT(CompandCompress, COMPAND_COMPRESS, compand_curve),\n> +       RKISP1_BLOCK_TYPE_ENTRY_EXT(Wdr, WDR, wdr),\n>  };\n>  \n>  } /* namespace */\n> diff --git a/src/ipa/rkisp1/params.h b/src/ipa/rkisp1/params.h\n> index 40450e34497a..2e60528d102e 100644\n> --- a/src/ipa/rkisp1/params.h\n> +++ b/src/ipa/rkisp1/params.h\n> @@ -40,6 +40,7 @@ enum class BlockType {\n>         CompandBls,\n>         CompandExpand,\n>         CompandCompress,\n> +       Wdr,\n>  };\n>  \n>  namespace details {\n> @@ -74,6 +75,7 @@ RKISP1_DEFINE_BLOCK_TYPE(Afc, afc)\n>  RKISP1_DEFINE_BLOCK_TYPE(CompandBls, compand_bls)\n>  RKISP1_DEFINE_BLOCK_TYPE(CompandExpand, compand_curve)\n>  RKISP1_DEFINE_BLOCK_TYPE(CompandCompress, compand_curve)\n> +RKISP1_DEFINE_BLOCK_TYPE(Wdr, wdr)\n>  \n>  } /* namespace details */\n>  \n> diff --git a/src/libcamera/control_ids_draft.yaml b/src/libcamera/control_ids_draft.yaml\n> index 03309eeac34f..b90d78841719 100644\n> --- a/src/libcamera/control_ids_draft.yaml\n> +++ b/src/libcamera/control_ids_draft.yaml\n> @@ -293,5 +293,67 @@ controls:\n>  \n>          Currently identical to ANDROID_STATISTICS_FACE_IDS.\n>        size: [n]\n\nSo ... my biggest push back here would be can we have these straight to\nnormal controls please?\n\nWe really want to avoid using ::draft::\n\n\n> +  - WdrMode:\n> +      type: int32_t\n> +      direction: inout\n> +      description: |\n> +        Set the WDR mode.\n> +\n> +        The WDR mode is used to select the algorithm used for global tone\n> +        mapping. It will automatically reduce the exposure time of the sensor\n> +        so that there are only a small number of saturated pixels in the image.\n> +        The algorithm then compensates for the loss of brightness by applying a\n> +        global tone mapping curve to the image.\n> +      enum:\n> +        - name: WdrOff\n> +          value: 0\n> +          description: Wdr is disabled.\n> +        - name: WdrLinear\n> +          value: 1\n> +          description:\n> +            Apply a linear global tone mapping curve.\n> +\n> +            A curve with two linear sections is applied. This produces good\n> +            results at the expense of a slightly artificial look.\n> +        - name: WdrPower\n> +          value: 2\n> +          description: |\n> +            Apply a power global tone mapping curve.\n> +\n> +            This curve has high gain values on the dark areas of an image and\n> +            high compression values on the bright area. It therefore tends to\n> +            produce noticeable noise artifacts.\n> +        - name: WdrExponential\n> +          value: 3\n> +          description: |\n> +            Apply an exponential global tone mapping curve.\n> +\n> +            This curve has lower gain values in dark areas compared to the power\n> +            curve but produces a more natural look compared to the linear curve.\n> +            It is therefore the best choice for most scenes.\n> +        - name: WdrHistogramEqualization\n> +          value: 4\n> +          description: |\n> +            Apply histogram equalization.\n> +\n> +            This curve preserves most of the information of the image at the\n> +            expense of a very artificial look. It is therefore best suited for\n> +            technical analysis.\n> +  - WdrStrength:\n> +      type: float\n> +      direction: in\n> +      description: |\n> +        Specify the strength of the wdr algorithm. The exact meaning of this\n> +        value is specific to the algorithm in use. Usually a value of 0 means no\n> +        global tone mapping is applied. A values of 1 is the default value and\n> +        the correct value for most scenes. A value above 1 increases the global\n> +        tone mapping effect and can lead to unrealistic image effects.\n> +  - WdrMaxBrightPixels:\n> +      type: float\n> +      direction: in\n> +      description: |\n> +        Percentage of allowed (nearly) saturated pixels. The WDR algorithm\n> +        reduces the WdrExposureValue until the amount of pixels that are close\n> +        to saturation is lower than this value.\n\nI'd be fine with all of those in the main control namespace I believe.\n\nMost comments above are about trivial nits ... so I'll go for..\n\nwith the controls moved out of ::draft::\n\n\nReviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>\n\n>  \n>  ...\n> -- \n> 2.48.1\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 30783C328C\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu, 18 Sep 2025 18:41:19 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 3F00269371;\n\tThu, 18 Sep 2025 20:41:18 +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 2EDFC62C3A\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 18 Sep 2025 20:41:17 +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 45ED0C6E;\n\tThu, 18 Sep 2025 20:39:57 +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=\"H3R4y8TU\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1758220797;\n\tbh=3/8hZlNUuTzZ7B4Iy1uh43RKRFNHLfUBnx/fIUHhov4=;\n\th=In-Reply-To:References:Subject:From:Cc:To:Date:From;\n\tb=H3R4y8TU/yzPBxLX0zs7SogAGByNDNdYHnrmNdbtezNuzBXHQHEzfFr9byV4MMSPd\n\t1eZlawxc8SCNUKP/rA7zaEiPfA7zthjh31jIBv0Y3wdKuKQ8nxVYf5UsiAq/9CSycz\n\tlCOLFxCJSpJ3fXRMaa81FjP0MGhnfl6wrTJHXbr0=","Content-Type":"text/plain; charset=\"utf-8\"","MIME-Version":"1.0","Content-Transfer-Encoding":"quoted-printable","In-Reply-To":"<20250918144333.108695-19-stefan.klug@ideasonboard.com>","References":"<20250918144333.108695-1-stefan.klug@ideasonboard.com>\n\t<20250918144333.108695-19-stefan.klug@ideasonboard.com>","Subject":"Re: [PATCH v4 18/19] ipa: rkisp1: Add WDR algorithm","From":"Kieran Bingham <kieran.bingham@ideasonboard.com>","Cc":"Stefan Klug <stefan.klug@ideasonboard.com>,\n\tPaul Elder <paul.elder@ideasonboard.com>","To":"Stefan Klug <stefan.klug@ideasonboard.com>,\n\tlibcamera-devel@lists.libcamera.org","Date":"Thu, 18 Sep 2025 19:41:13 +0100","Message-ID":"<175822087394.3990114.6026778704604652527@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>"}},{"id":35917,"web_url":"https://patchwork.libcamera.org/comment/35917/","msgid":"<175827411258.17312.7120433553558011292@localhost>","date":"2025-09-19T09:28:32","subject":"Re: [PATCH v4 18/19] ipa: rkisp1: Add WDR algorithm","submitter":{"id":184,"url":"https://patchwork.libcamera.org/api/people/184/","name":"Stefan Klug","email":"stefan.klug@ideasonboard.com"},"content":"Hi Kieran,\n\nThank you for the review.\n\nQuoting Kieran Bingham (2025-09-18 20:41:13)\n> Quoting Stefan Klug (2025-09-18 15:43:27)\n> > Add a WDR algorithm to do global tone mapping. Global tone mapping is\n> > used to increase the perceived dynamic range of an image. The typical\n> > effect is that in areas that are normally overexposed, additional\n> > structure becomes visible.\n> > \n> > The overall idea is that the algorithm applies an exposure value\n> > correction to underexpose the image to the point where only a small\n> > number of saturated pixels is left. This artificial underexposure is\n> > then mitigated by applying a tone mapping curve.\n> > \n> > This algorithm implements 4 tone mapping strategies:\n> > - Linear\n> > - Power\n> > - Exponential\n> > - Histogram equalization\n> > \n> > Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com>\n> > Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>\n> > \n> > ---\n> > \n> > Changes in v4:\n> > - Properly initialize activeState.wdr.gain\n> > - Fixed some typos and improved wording\n> > \n> > Changes in v3:\n> > - Removed the need for a separate regulation loop, by not relying on an\n> >   added ExposureValue but by applying a constraint to AGC regulation and\n> > deducing the required WDR gain from the histogram. This makes the\n> > structure easier and hopefully less prone to oscillations.\n> > - Dropped debug metadata as this needs more discussions and is not\n> >   required for the algorithm to work.\n> > - Dropped minExposureValue as it is not used anymore.\n> > - Added a damping on the gain applied by the WDR curve\n> > - Moved strength into activeState so that it is reset on configure()\n> > \n> > Changes in v2:\n> > - Fixed default value for min bright pixels\n> > - Added check for supported params type\n> > - Reset PID controller\n> > - Various fixes from Pauls review\n> > ---\n> >  src/ipa/rkisp1/algorithms/agc.cpp     |   7 +-\n> >  src/ipa/rkisp1/algorithms/meson.build |   1 +\n> >  src/ipa/rkisp1/algorithms/wdr.cpp     | 493 ++++++++++++++++++++++++++\n> >  src/ipa/rkisp1/algorithms/wdr.h       |  58 +++\n> >  src/ipa/rkisp1/ipa_context.h          |  14 +\n> >  src/ipa/rkisp1/params.cpp             |   1 +\n> >  src/ipa/rkisp1/params.h               |   2 +\n> >  src/libcamera/control_ids_draft.yaml  |  62 ++++\n> >  8 files changed, 637 insertions(+), 1 deletion(-)\n> >  create mode 100644 src/ipa/rkisp1/algorithms/wdr.cpp\n> >  create mode 100644 src/ipa/rkisp1/algorithms/wdr.h\n> > \n> > diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp\n> > index 046a1ac9caa2..f7ea4c70e2ae 100644\n> > --- a/src/ipa/rkisp1/algorithms/agc.cpp\n> > +++ b/src/ipa/rkisp1/algorithms/agc.cpp\n> > @@ -594,7 +594,12 @@ void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,\n> >                 maxAnalogueGain = frameContext.agc.gain;\n> >         }\n> >  \n> > -       setLimits(minExposureTime, maxExposureTime, minAnalogueGain, maxAnalogueGain, {});\n> > +       std::vector<AgcMeanLuminance::AgcConstraint> additionalConstraints;\n> > +       if (context.activeState.wdr.mode != controls::draft::WdrOff)\n> > +               additionalConstraints.push_back(context.activeState.wdr.constraint);\n> > +\n> > +       setLimits(minExposureTime, maxExposureTime, minAnalogueGain, maxAnalogueGain,\n> > +                 std::move(additionalConstraints));\n> >  \n> >         /*\n> >          * The Agc algorithm needs to know the effective exposure value that was\n> > diff --git a/src/ipa/rkisp1/algorithms/meson.build b/src/ipa/rkisp1/algorithms/meson.build\n> > index 2e42a80cf99d..d329dbfb432d 100644\n> > --- a/src/ipa/rkisp1/algorithms/meson.build\n> > +++ b/src/ipa/rkisp1/algorithms/meson.build\n> > @@ -14,4 +14,5 @@ rkisp1_ipa_algorithms = files([\n> >      'gsl.cpp',\n> >      'lsc.cpp',\n> >      'lux.cpp',\n> > +    'wdr.cpp',\n> >  ])\n> > diff --git a/src/ipa/rkisp1/algorithms/wdr.cpp b/src/ipa/rkisp1/algorithms/wdr.cpp\n> > new file mode 100644\n> > index 000000000000..a4acf094388f\n> > --- /dev/null\n> > +++ b/src/ipa/rkisp1/algorithms/wdr.cpp\n> > @@ -0,0 +1,493 @@\n> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> > +/*\n> > + * Copyright (C) 2025, Ideas On Board\n> > + *\n> > + * RkISP1 Wide Dynamic Range control\n> > + */\n> > +\n> > +#include \"wdr.h\"\n> > +\n> > +#include <libcamera/base/log.h>\n> > +#include <libcamera/base/utils.h>\n> > +\n> > +#include \"libcamera/internal/yaml_parser.h\"\n> > +\n> > +#include <libipa/agc_mean_luminance.h>\n> > +#include <libipa/histogram.h>\n> > +#include <libipa/pwl.h>\n> > +\n> > +#include \"linux/rkisp1-config.h\"\n> > +\n> > +/**\n> > + * \\file wdr.h\n> > + */\n> > +\n> > +namespace libcamera {\n> > +\n> > +namespace ipa::rkisp1::algorithms {\n> > +\n> > +/**\n> > + * \\class WideDynamicRange\n> > + * \\brief RkISP1 Wide Dynamic Range algorithm\n> > + *\n> > + * This algorithm implements automatic global tone mapping for the RkISP1.\n> > + * Global tone mapping is done by the GWDR hardware block and applies\n> > + * a global tone mapping curve to the image to increase the perceived dynamic\n> > + * range. Imagine an indoor scene with bright outside visible through the\n> > + * windows. With normal exposure settings, the windows will be completely\n> > + * saturated and no structure (sky/clouds) will be visible because the AEGC has\n> > + * to increase overall exposure to reach a certain level of mean brightness. In\n> > + * WDR mode, the algorithm will artifically reduce the exposure time so that the\n> > + * texture and colours become visible in the formerly saturated areas. Then the\n> > + * global tone mapping curve is applied to mitigate the loss of brightness.\n> > + *\n> > + * Calculating that tone mapping curve is the most difficult part. This\n> > + * algorithm implements four tone mapping strategies:\n> > + * - Linear: The tone mapping curve is a combination of two linear functions\n> > + *   with one kneepoint\n> > + * - Power: The tone mapping curve follows a power function\n> > + * - Exponential: The tone mapping curve follows an exponential function\n> > + * - HistogramEqualization: The tone mapping curve tries to equalize the\n> > + *   histogram\n> > + *\n> > + * The overall strategy is the same in all cases: Add a constraint to the AEGC\n> > + * regulation so that the number of nearly saturated pixels goes below a given\n> > + * threshold (default 2%). This threshold can either be specified in the tuning\n> > + * file or set via the WdrMaxBrightPixels control.\n> > + *\n> > + * The global tone mapping curve is then calculated so that it accounts for the\n> > + * reduction of brightness due to the exposure constraint. We'll call this the\n> > + * WDR-gain. As the result of tone mapping is very difficult to quantize and is\n> > + * by definition a lossy process there is not a single \"correct\" solution on how\n> > + * this curve should look like.\n> > + *\n> > + * The approach taken here is based on a simple linear model. Consider a pixel\n> > + * that was originally 50% grey. It will have its exposure pushed down by the\n> > + * WDR's initial exposure compensation. This value then needs to be pushed back\n> > + * up by the tone mapping curve so that it is 50% grey again. This point serves\n> > + * as our kneepoint. To get to this kneepoint, this pixel and all darker pixels\n> > + * (to the left of the kneepoint on the tone mapping curve) will simply have the\n> > + * exposure compensation undone by WDR-gain. This cancels out the\n> > + * original exposure compensation, which was 1/WDR-gain. The remaining\n> > + * brigher pixels (to the right of the kneepoint on the tone mapping curve) will\n> > + * be compressed. The WdrStrength control adjusts the gain of the left part of\n> > + * the tone mapping curve.\n> > + *\n> > + * In the Power and Exponential modes, the curves are calculated so that they\n> > + * pass through that kneepoint.\n> > + *\n> > + * The histogram equalization mode tries to equalize the histogram of the\n> > + * image and acts independently of the calculated exposure value.\n> > + *\n> > + * \\code{.unparsed}\n> > + * algorithms:\n> > + *   - WideDynamicRange:\n> > + *       ExposureConstraint:\n> > + *         MaxBrightPixels: 0.02\n> > + *         yTarget: 0.95\n> > + * \\endcode\n> > + */\n> > +\n> > +LOG_DEFINE_CATEGORY(RkISP1Wdr)\n> > +\n> > +static constexpr unsigned int kTonecurveXIntervals = RKISP1_CIF_ISP_WDR_CURVE_NUM_INTERV;\n> > +\n> > +/*\n> > + * Increasing interval sizes. The intervals are crafted so that they sum\n> > + * up to 4096. This results in better fitting curves than the constant intervals\n> > + * (all entries are 4)\n> > + */\n> > +static constexpr std::array<int, kTonecurveXIntervals> kLoglikeIntervals = {\n> > +       { 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4,\n> > +         4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6 }\n> > +};\n> > +\n> > +WideDynamicRange::WideDynamicRange()\n> > +{\n> > +}\n> > +\n> > +/**\n> > + * \\copydoc libcamera::ipa::Algorithm::init\n> > + */\n> > +int WideDynamicRange::init([[maybe_unused]] IPAContext &context,\n> > +                          [[maybe_unused]] const YamlObject &tuningData)\n> > +{\n> > +       if (!(context.hw.supportedBlocks & 1 << RKISP1_EXT_PARAMS_BLOCK_TYPE_WDR)) {\n> > +               LOG(RkISP1Wdr, Error)\n> > +                       << \"Wide Dynamic Range not supported by the hardware or kernel.\";\n> > +               return -ENOTSUP;\n> > +       }\n> > +\n> > +       toneCurveIntervalValues_ = kLoglikeIntervals;\n> > +\n> > +       /* Calculate a list of normed x values */\n> > +       toneCurveX_[0] = 0.0;\n> > +       int lastValue = 0;\n> > +       for (unsigned int i = 1; i < toneCurveX_.size(); i++) {\n> > +               lastValue += std::pow(2, toneCurveIntervalValues_[i - 1] + 3);\n> > +               lastValue = std::min(lastValue, 4096);\n> > +               toneCurveX_[i] = lastValue / 4096.0;\n> > +       }\n> > +\n> > +       exposureConstraintMaxBrightPixels_ = 0.02;\n> > +       exposureConstraintY_ = 0.95;\n> > +\n> > +       const auto &constraint = tuningData[\"ExposureConstraint\"];\n> > +       if (!constraint.isDictionary()) {\n> > +               LOG(RkISP1Wdr, Warning)\n> > +                       << \"ExposureConstraint not found in tuning data.\"\n> > +                          \"Using default values MaxBrightPixels: \"\n> > +                       << exposureConstraintMaxBrightPixels_\n> > +                       << \" yTarget: \" << exposureConstraintY_;\n> > +       } else {\n> > +               exposureConstraintMaxBrightPixels_ =\n> > +                       constraint[\"MaxBrightPixels\"]\n> > +                               .get<double>()\n> > +                               .value_or(exposureConstraintMaxBrightPixels_);\n> > +               exposureConstraintY_ =\n> > +                       constraint[\"yTarget\"]\n> > +                               .get<double>()\n> > +                               .value_or(exposureConstraintY_);\n> > +       }\n> > +\n> > +       context.ctrlMap[&controls::draft::WdrMode] =\n> > +               ControlInfo(controls::draft::WdrModeValues, controls::draft::WdrOff);\n> > +       context.ctrlMap[&controls::draft::WdrStrength] =\n> > +               ControlInfo(0.0f, 2.0f, 1.0f);\n> > +       context.ctrlMap[&controls::draft::WdrMaxBrightPixels] =\n> > +               ControlInfo(0.0f, 1.0f, static_cast<float>(exposureConstraintMaxBrightPixels_));\n> > +\n> > +       applyCompensationLinear(1.0, 0.0);\n> > +\n> > +       return 0;\n> > +}\n> > +\n> > +/**\n> > + * \\copydoc libcamera::ipa::Algorithm::configure\n> > + */\n> > +int WideDynamicRange::configure(IPAContext &context,\n> > +                               [[maybe_unused]] const IPACameraSensorInfo &configInfo)\n> > +{\n> > +       context.activeState.wdr.mode = controls::draft::WdrOff;\n> > +       context.activeState.wdr.gain = 1.0;\n> > +       context.activeState.wdr.strength = 1.0;\n> > +       auto &constraint = context.activeState.wdr.constraint;\n> > +       constraint.bound = AgcMeanLuminance::AgcConstraint::Bound::Upper;\n> > +       constraint.qHi = 1.0;\n> > +       constraint.qLo = 1.0 - exposureConstraintMaxBrightPixels_;\n> > +       constraint.yTarget = exposureConstraintY_;\n> > +       return 0;\n> > +}\n> > +\n> > +void WideDynamicRange::applyHistogramEqualization(double strength)\n> > +{\n> > +       if (hist_.empty())\n> > +               return;\n> > +\n> > +       /**\n> \n> When doxygen builds this - does it make sense to have this as a\n> doxygen block ?\n> \n> > +        * Apply a factor on strength, so that it roughly matches the optical\n> > +        * impression that is produced by the other algorithms. The goal is that\n> > +        * the user can switch algorithms for different looks but similar\n> > +        * \"strength\".\n> > +        */\n> > +       strength *= 0.65;\n> > +\n> > +       /**\n> \n> Same.\n> \n> > +        * In a fully equalized histogram, all bins have the same value. Try\n> > +        * to equalize the histogram by applying a gain or damping depending on\n> > +        * the distance of the actual bin value from that norm.\n> > +        */\n> > +       std::vector<double> gains;\n> > +       gains.resize(hist_.size());\n> > +       double sum = 0;\n> > +       double norm = 1.0 / (gains.size());\n> > +       for (unsigned i = 0; i < hist_.size(); i++) {\n> > +               double diff = 1.0 + strength * (hist_[i] - norm) / norm;\n> > +               gains[i] = diff;\n> > +               sum += diff;\n> > +       }\n> > +\n> > +       /* Never amplify the last entry. */\n> > +       gains.back() = std::max(gains.back(), 1.0);\n> > +\n> > +       double scale = gains.size() / sum;\n> > +       for (auto &v : gains)\n> > +               v *= scale;\n> > +\n> > +       Pwl pwl;\n> > +       double step = 1.0 / gains.size();\n> > +       double lastX = 0;\n> > +       double lastY = 0;\n> > +\n> > +       pwl.append(lastX, lastY);\n> > +       for (unsigned int i = 0; i < gains.size() - 1; i++) {\n> > +               lastY += gains[i] * step;\n> > +               lastX += step;\n> > +               pwl.append(lastX, lastY);\n> > +       }\n> > +       pwl.append(1.0, 1.0);\n> > +\n> > +       for (unsigned int i = 0; i < toneCurveX_.size(); i++)\n> > +               toneCurveY_[i] = pwl.eval(toneCurveX_[i]);\n> > +}\n> > +\n> > +Vector<double, 2> WideDynamicRange::kneePoint(double gain, double strength)\n> > +{\n> > +       gain = std::pow(gain, strength);\n> > +       double y = 0.5;\n> > +       double x = y / gain;\n> > +\n> > +       return { { x, y } };\n> > +}\n> > +\n> > +void WideDynamicRange::applyCompensationLinear(double gain, double strength)\n> > +{\n> > +       auto kp = kneePoint(gain, strength);\n> > +       double g1 = kp.y() / kp.x();\n> > +       double g2 = (kp.y() - 1) / (kp.x() - 1);\n> > +\n> > +       for (unsigned int i = 0; i < toneCurveX_.size(); i++) {\n> > +               double x = toneCurveX_[i];\n> > +               double y;\n> > +               if (x <= kp.x()) {\n> > +                       y = g1 * x;\n> > +               } else {\n> > +                       y = g2 * x + 1 - g2;\n> > +               }\n> > +               toneCurveY_[i] = y;\n> > +       }\n> > +}\n> > +\n> > +void WideDynamicRange::applyCompensationPower(double gain, double strength)\n> > +{\n> > +       double e = 1.0;\n> > +       if (strength > 1e-6) {\n> > +               auto kp = kneePoint(gain, strength);\n> > +               /* Calculate an exponent to go through the knee point. */\n> > +               e = log(kp.y()) / log(kp.x());\n> > +       }\n> > +\n> > +       /**\n> \n> Same ...\n> \n> But I do love these documentation blocks. I wonder if the 'IPA' docs\n> would almost build into a dedicated page somehow with all of this\n> content in the future.\n> \n> \n> > +        * The power function tends to be extremely steep at the beginning. This\n> > +        * leads to noise and image artifacts in the dark areas. To mitigate\n> > +        * that, we add a short linear section at the beginning of the curve.\n> > +        * The connection between linear and power is the point where the linear\n> > +        * section reaches the y level yLin. The power curve is then scaled so\n> > +        * that it starts at the connection point with the steepness it would\n> > +        * have at y=yLin but still goes through 1,1\n> > +        **/\n> \n> I think even doxygen uses a single * at the end ?\n> \n> > +       double yLin = 0.1;\n> > +       /* x position of the connection point */\n> > +       double xb = yLin / gain;\n> > +       /* x offset for the scaled power function */\n> > +       double q = xb - std::exp(std::log(yLin) / e);\n> > +\n> > +       for (unsigned int i = 0; i < toneCurveX_.size(); i++) {\n> > +               double x = toneCurveX_[i];\n> > +               if (x < xb) {\n> > +                       toneCurveY_[i] = x * gain;\n> > +               } else {\n> > +                       x = (x - q) / (1 - q);\n> > +                       toneCurveY_[i] = std::pow(x, e);\n> > +               }\n> > +       }\n> > +}\n> > +\n> > +void WideDynamicRange::applyCompensationExponential(double gain, double strength)\n> > +{\n> > +       double k = 0.1;\n> > +       auto kp = kneePoint(gain, strength);\n> > +       double kx = kp.x();\n> > +       double ky = kp.y();\n> > +\n> > +       if (kx > ky) {\n> > +               LOG(RkISP1Wdr, Warning) << \"Invalid knee point: \" << kp;\n> > +               kx = ky;\n> > +       }\n> > +\n> > +       /*\n> > +        * The exponential curve is based on the function proposed by Glozman\n> > +        * et al. in\n> > +        * S. Glozman, T. Kats, and O. Yadid-Pecht, \"Exponent Operator Based\n> > +        * Tone Mapping Algorithm for Color Wide Dynamic Range Images,\" 2011.\n> > +        *\n> > +        * That function uses a k factor as parameter for the WDR compression\n> > +        * curve:\n> > +        * k=0: maximum compression\n> > +        * k=infinity: linear curve\n> > +        *\n> > +        * To calculate a k factor that results in a curve that passes through\n> > +        * the kneepoint, the equation needs to be solved for k after inserting\n> > +        * the kneepoint.  This can be formulated as search for a zero point.\n> > +        * Unfortunately there is no closed solution for that transformation.\n> > +        * Using newton's method to approximate the value is numerically\n> > +        * unstable.\n> > +        *\n> > +        * Luckily the function only crosses the x axis once and for the set of\n> > +        * possible kneepoints, a negative and a positive point can be guessed.\n> > +        * The approximation is then implemented using bisection.\n> > +        */\n> > +       if (std::abs(kx - ky) < 0.001) {\n> > +               k = 1e8;\n> > +       } else {\n> > +               double kl = 0.0001;\n> > +               double kh = 1000;\n> > +\n> > +               auto fk = [=](double v) {\n> > +                       return std::exp(-kx / v) -\n> > +                              ky * std::exp(-1.0 / v) + ky - 1.0;\n> > +               };\n> > +\n> > +               ASSERT(fk(kl) < 0);\n> > +               ASSERT(fk(kh) > 0);\n> > +\n> > +               k = kh / 10.0;\n> > +               while (fk(k) > 0) {\n> > +                       kh = k;\n> > +                       k /= 10.0;\n> > +               }\n> > +\n> > +               do {\n> > +                       k = (kl + kh) / 2;\n> > +                       if (fk(k) < 0)\n> > +                               kl = k;\n> > +                       else\n> > +                               kh = k;\n> > +               } while (std::abs(kh - kl) > 1e-3);\n> > +       }\n> > +\n> > +       double a = 1.0 / (1.0 - std::exp(-1.0 / k));\n> > +       for (unsigned int i = 0; i < toneCurveX_.size(); i++)\n> > +               toneCurveY_[i] = a * (1.0 - std::exp(-toneCurveX_[i] / k));\n> > +}\n> > +\n> > +/**\n> > + * \\copydoc libcamera::ipa::Algorithm::queueRequest\n> > + */\n> > +void WideDynamicRange::queueRequest([[maybe_unused]] IPAContext &context,\n> > +                                   [[maybe_unused]] const uint32_t frame,\n> > +                                   IPAFrameContext &frameContext,\n> > +                                   const ControlList &controls)\n> > +{\n> > +       auto &activeState = context.activeState;\n> > +\n> > +       const auto &mode = controls.get(controls::draft::WdrMode);\n> > +       if (mode)\n> > +               activeState.wdr.mode = static_cast<controls::draft::WdrModeEnum>(*mode);\n> > +\n> > +       const auto &brightPixels = controls.get(controls::draft::WdrMaxBrightPixels);\n> > +       if (brightPixels)\n> > +               activeState.wdr.constraint.qLo = 1.0 - *brightPixels;\n> > +\n> > +       const auto &strength = controls.get(controls::draft::WdrStrength);\n> > +       if (strength)\n> > +               activeState.wdr.strength = *strength;\n> > +\n> > +       frameContext.wdr.mode = activeState.wdr.mode;\n> > +       frameContext.wdr.strength = activeState.wdr.strength;\n> > +}\n> > +\n> > +/**\n> > + * \\copydoc libcamera::ipa::Algorithm::prepare\n> > + */\n> > +void WideDynamicRange::prepare(IPAContext &context,\n> > +                              [[maybe_unused]] const uint32_t frame,\n> > +                              IPAFrameContext &frameContext,\n> > +                              RkISP1Params *params)\n> > +{\n> > +       if (!params) {\n> > +               LOG(RkISP1Wdr, Warning) << \"Params is null\";\n> > +               return;\n> > +       }\n> > +\n> > +       auto mode = frameContext.wdr.mode;\n> > +\n> > +       auto config = params->block<BlockType::Wdr>();\n> > +       config.setEnabled(mode != controls::draft::WdrOff);\n> > +\n> > +       /* Calculate how much EV we need to compensate with the WDR curve. */\n> > +       double gain = context.activeState.wdr.gain;\n> > +       frameContext.wdr.gain = gain;\n> > +\n> > +       if (mode == controls::draft::WdrOff) {\n> > +               applyCompensationLinear(1.0, 0.0);\n> > +       } else if (mode == controls::draft::WdrLinear) {\n> > +               applyCompensationLinear(gain, frameContext.wdr.strength);\n> > +       } else if (mode == controls::draft::WdrPower) {\n> > +               applyCompensationPower(gain, frameContext.wdr.strength);\n> > +       } else if (mode == controls::draft::WdrExponential) {\n> > +               applyCompensationExponential(gain, frameContext.wdr.strength);\n> > +       } else if (mode == controls::draft::WdrHistogramEqualization) {\n> > +               applyHistogramEqualization(frameContext.wdr.strength);\n> > +       }\n> > +\n> > +       /* Reset value */\n> > +       config->dmin_strength = 0x10;\n> > +       config->dmin_thresh = 0;\n> > +\n> > +       for (unsigned int i = 0; i < kTonecurveXIntervals; i++) {\n> > +               int v = toneCurveIntervalValues_[i];\n> > +               config->tone_curve.dY[i / 8] |= (v & 0x07) << ((i % 8) * 4);\n> > +       }\n> > +\n> > +       /*\n> > +        * Fix the curve to adhere to the hardware constraints. Don't apply a\n> > +        * constraint on the first element, which is most likely zero anyways.\n> > +        */\n> > +       int lastY = toneCurveY_[0] * 4096.0;\n> > +       for (unsigned int i = 0; i < toneCurveX_.size(); i++) {\n> > +               int diff = static_cast<int>(toneCurveY_[i] * 4096.0) - lastY;\n> > +               diff = std::clamp(diff, -2048, 2048);\n> > +               lastY = lastY + diff;\n> > +               config->tone_curve.ym[i] = lastY;\n> > +       }\n> > +}\n> > +\n> > +void WideDynamicRange::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,\n> > +                              IPAFrameContext &frameContext,\n> > +                              const rkisp1_stat_buffer *stats,\n> > +                              ControlList &metadata)\n> > +{\n> > +       if (!stats || !(stats->meas_type & RKISP1_CIF_ISP_STAT_HIST)) {\n> > +               LOG(RkISP1Wdr, Warning) << \"No histogram data in statistics\";\n> > +               return;\n> > +       }\n> > +\n> > +       const rkisp1_cif_isp_stat *params = &stats->params;\n> > +       auto mode = frameContext.wdr.mode;\n> > +\n> > +       metadata.set(controls::draft::WdrMode, mode);\n> > +\n> > +       Histogram cumHist({ params->hist.hist_bins, context.hw.numHistogramBins },\n> > +                         [](uint32_t x) { return x >> 4; });\n> > +\n> > +       /* Calculate the gain needed to reach the requested yTarget*/\n> \n> space between yTarget and */\n> \n> > +       double value = cumHist.interQuantileMean(0, 1.0) / cumHist.bins();\n> > +       double gain = context.activeState.agc.automatic.yTarget / value;\n> > +       gain = std::max(gain, 1.0);\n> > +\n> > +       double speed = 0.2;\n> > +       gain = gain * speed + context.activeState.wdr.gain * (1.0 - speed);\n> > +\n> > +       context.activeState.wdr.gain = gain;\n> > +\n> > +       std::vector<double> hist;\n> > +       double sum = 0;\n> > +       for (unsigned i = 0; i < context.hw.numHistogramBins; i++) {\n> > +               double v = params->hist.hist_bins[i] >> 4;\n> > +               hist.push_back(v);\n> > +               sum += v;\n> > +       }\n> > +\n> > +       /* Scale so that the entries sum up to 1. */\n> > +       double scale = 1.0 / sum;\n> > +       for (auto &v : hist)\n> > +               v *= scale;\n> > +       hist_.swap(hist);\n> > +}\n> > +\n> > +REGISTER_IPA_ALGORITHM(WideDynamicRange, \"WideDynamicRange\")\n> > +\n> > +} /* namespace ipa::rkisp1::algorithms */\n> > +\n> > +} /* namespace libcamera */\n> > diff --git a/src/ipa/rkisp1/algorithms/wdr.h b/src/ipa/rkisp1/algorithms/wdr.h\n> > new file mode 100644\n> > index 000000000000..90548ce9e840\n> > --- /dev/null\n> > +++ b/src/ipa/rkisp1/algorithms/wdr.h\n> > @@ -0,0 +1,58 @@\n> > +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> > +/*\n> > + * Copyright (C) 2021-2022, Ideas On Board\n> \n> Perhaps a newer date here.\n> \n> > + *\n> > + * RkISP1 Wide Dynamic Range control\n> > + */\n> > +\n> > +#pragma once\n> > +\n> > +#include <libcamera/control_ids.h>\n> > +\n> > +#include \"linux/rkisp1-config.h\"\n> > +\n> > +#include \"algorithm.h\"\n> > +\n> > +namespace libcamera {\n> > +\n> > +namespace ipa::rkisp1::algorithms {\n> > +\n> > +class WideDynamicRange : public Algorithm\n> > +{\n> > +public:\n> > +       WideDynamicRange();\n> > +       ~WideDynamicRange() = default;\n> > +\n> > +       int init(IPAContext &context, const YamlObject &tuningData) override;\n> > +       int configure(IPAContext &context, const IPACameraSensorInfo &configInfo) override;\n> > +\n> > +       void queueRequest(IPAContext &context, const uint32_t frame,\n> > +                         IPAFrameContext &frameContext,\n> > +                         const ControlList &controls) override;\n> > +       void prepare(IPAContext &context, const uint32_t frame,\n> > +                    IPAFrameContext &frameContext,\n> > +                    RkISP1Params *params) override;\n> > +       void process(IPAContext &context, const uint32_t frame,\n> > +                    IPAFrameContext &frameContext,\n> > +                    const rkisp1_stat_buffer *stats,\n> > +                    ControlList &metadata) override;\n> > +\n> > +private:\n> > +       Vector<double, 2> kneePoint(double gain, double strength);\n> > +       void applyCompensationLinear(double gain, double strength);\n> > +       void applyCompensationPower(double gain, double strength);\n> > +       void applyCompensationExponential(double gain, double strength);\n> > +       void applyHistogramEqualization(double strength);\n> > +\n> > +       double exposureConstraintMaxBrightPixels_;\n> > +       double exposureConstraintY_;\n> > +\n> > +       std::vector<double> hist_;\n> > +\n> > +       std::array<int, RKISP1_CIF_ISP_WDR_CURVE_NUM_INTERV> toneCurveIntervalValues_;\n> > +       std::array<double, RKISP1_CIF_ISP_WDR_CURVE_NUM_INTERV + 1> toneCurveX_;\n> > +       std::array<double, RKISP1_CIF_ISP_WDR_CURVE_NUM_INTERV + 1> toneCurveY_;\n> > +};\n> > +\n> > +} /* namespace ipa::rkisp1::algorithms */\n> > +} /* namespace libcamera */\n> > diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h\n> > index 113b90428008..a662e37c4079 100644\n> > --- a/src/ipa/rkisp1/ipa_context.h\n> > +++ b/src/ipa/rkisp1/ipa_context.h\n> > @@ -26,6 +26,7 @@\n> >  \n> >  #include <libipa/camera_sensor_helper.h>\n> >  #include <libipa/fc_queue.h>\n> > +#include \"libipa/agc_mean_luminance.h\"\n> >  \n> >  namespace libcamera {\n> >  \n> > @@ -131,6 +132,13 @@ struct IPAActiveState {\n> >         struct {\n> >                 double gamma;\n> >         } goc;\n> > +\n> > +       struct {\n> > +               controls::draft::WdrModeEnum mode;\n> > +               AgcMeanLuminance::AgcConstraint constraint;\n> > +               double gain;\n> > +               double strength;\n> > +       } wdr;\n> >  };\n> >  \n> >  struct IPAFrameContext : public FrameContext {\n> > @@ -200,6 +208,12 @@ struct IPAFrameContext : public FrameContext {\n> >         struct {\n> >                 double lux;\n> >         } lux;\n> > +\n> > +       struct {\n> > +               controls::draft::WdrModeEnum mode;\n> > +               double strength;\n> > +               double gain;\n> > +       } wdr;\n> >  };\n> >  \n> >  struct IPAContext {\n> > diff --git a/src/ipa/rkisp1/params.cpp b/src/ipa/rkisp1/params.cpp\n> > index 4c0b051ce65d..5edb36c91b87 100644\n> > --- a/src/ipa/rkisp1/params.cpp\n> > +++ b/src/ipa/rkisp1/params.cpp\n> > @@ -74,6 +74,7 @@ const std::map<BlockType, BlockTypeInfo> kBlockTypeInfo = {\n> >         RKISP1_BLOCK_TYPE_ENTRY_EXT(CompandBls, COMPAND_BLS, compand_bls),\n> >         RKISP1_BLOCK_TYPE_ENTRY_EXT(CompandExpand, COMPAND_EXPAND, compand_curve),\n> >         RKISP1_BLOCK_TYPE_ENTRY_EXT(CompandCompress, COMPAND_COMPRESS, compand_curve),\n> > +       RKISP1_BLOCK_TYPE_ENTRY_EXT(Wdr, WDR, wdr),\n> >  };\n> >  \n> >  } /* namespace */\n> > diff --git a/src/ipa/rkisp1/params.h b/src/ipa/rkisp1/params.h\n> > index 40450e34497a..2e60528d102e 100644\n> > --- a/src/ipa/rkisp1/params.h\n> > +++ b/src/ipa/rkisp1/params.h\n> > @@ -40,6 +40,7 @@ enum class BlockType {\n> >         CompandBls,\n> >         CompandExpand,\n> >         CompandCompress,\n> > +       Wdr,\n> >  };\n> >  \n> >  namespace details {\n> > @@ -74,6 +75,7 @@ RKISP1_DEFINE_BLOCK_TYPE(Afc, afc)\n> >  RKISP1_DEFINE_BLOCK_TYPE(CompandBls, compand_bls)\n> >  RKISP1_DEFINE_BLOCK_TYPE(CompandExpand, compand_curve)\n> >  RKISP1_DEFINE_BLOCK_TYPE(CompandCompress, compand_curve)\n> > +RKISP1_DEFINE_BLOCK_TYPE(Wdr, wdr)\n> >  \n> >  } /* namespace details */\n> >  \n> > diff --git a/src/libcamera/control_ids_draft.yaml b/src/libcamera/control_ids_draft.yaml\n> > index 03309eeac34f..b90d78841719 100644\n> > --- a/src/libcamera/control_ids_draft.yaml\n> > +++ b/src/libcamera/control_ids_draft.yaml\n> > @@ -293,5 +293,67 @@ controls:\n> >  \n> >          Currently identical to ANDROID_STATISTICS_FACE_IDS.\n> >        size: [n]\n> \n> So ... my biggest push back here would be can we have these straight to\n> normal controls please?\n> \n> We really want to avoid using ::draft::\n> \n> \n> > +  - WdrMode:\n> > +      type: int32_t\n> > +      direction: inout\n> > +      description: |\n> > +        Set the WDR mode.\n> > +\n> > +        The WDR mode is used to select the algorithm used for global tone\n> > +        mapping. It will automatically reduce the exposure time of the sensor\n> > +        so that there are only a small number of saturated pixels in the image.\n> > +        The algorithm then compensates for the loss of brightness by applying a\n> > +        global tone mapping curve to the image.\n> > +      enum:\n> > +        - name: WdrOff\n> > +          value: 0\n> > +          description: Wdr is disabled.\n> > +        - name: WdrLinear\n> > +          value: 1\n> > +          description:\n> > +            Apply a linear global tone mapping curve.\n> > +\n> > +            A curve with two linear sections is applied. This produces good\n> > +            results at the expense of a slightly artificial look.\n> > +        - name: WdrPower\n> > +          value: 2\n> > +          description: |\n> > +            Apply a power global tone mapping curve.\n> > +\n> > +            This curve has high gain values on the dark areas of an image and\n> > +            high compression values on the bright area. It therefore tends to\n> > +            produce noticeable noise artifacts.\n> > +        - name: WdrExponential\n> > +          value: 3\n> > +          description: |\n> > +            Apply an exponential global tone mapping curve.\n> > +\n> > +            This curve has lower gain values in dark areas compared to the power\n> > +            curve but produces a more natural look compared to the linear curve.\n> > +            It is therefore the best choice for most scenes.\n> > +        - name: WdrHistogramEqualization\n> > +          value: 4\n> > +          description: |\n> > +            Apply histogram equalization.\n> > +\n> > +            This curve preserves most of the information of the image at the\n> > +            expense of a very artificial look. It is therefore best suited for\n> > +            technical analysis.\n> > +  - WdrStrength:\n> > +      type: float\n> > +      direction: in\n> > +      description: |\n> > +        Specify the strength of the wdr algorithm. The exact meaning of this\n> > +        value is specific to the algorithm in use. Usually a value of 0 means no\n> > +        global tone mapping is applied. A values of 1 is the default value and\n> > +        the correct value for most scenes. A value above 1 increases the global\n> > +        tone mapping effect and can lead to unrealistic image effects.\n> > +  - WdrMaxBrightPixels:\n> > +      type: float\n> > +      direction: in\n> > +      description: |\n> > +        Percentage of allowed (nearly) saturated pixels. The WDR algorithm\n> > +        reduces the WdrExposureValue until the amount of pixels that are close\n> > +        to saturation is lower than this value.\n> \n> I'd be fine with all of those in the main control namespace I believe.\n> \n> Most comments above are about trivial nits ... so I'll go for..\n> \n> with the controls moved out of ::draft::\n> \n> \n> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>\n\nThank you. I fixed all the nits you mentioned above and moved the\ncontrols into core. Will repost a v5 and then merge :-)\n\nRegards,\nStefan\n\n> \n> >  \n> >  ...\n> > -- \n> > 2.48.1\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 AFDF7BE173\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 19 Sep 2025 09:28:40 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 1AC4F6B599;\n\tFri, 19 Sep 2025 11:28:39 +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 8DB5262C38\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 19 Sep 2025 11:28:36 +0200 (CEST)","from ideasonboard.com (unknown\n\t[IPv6:2a00:6020:448c:6c00:4d54:eab8:98ca:163b])\n\tby perceval.ideasonboard.com (Postfix) with UTF8SMTPSA id E57147E4;\n\tFri, 19 Sep 2025 11:27:15 +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=\"aA8bsRi6\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1758274036;\n\tbh=gsTAMy2D+1vaviiT+7XvePW2NLYJe6PDv1danRFrH+c=;\n\th=In-Reply-To:References:Subject:From:Cc:To:Date:From;\n\tb=aA8bsRi6Jr22NoDTU3W25Gg0Qko4qL9zjXtcLtIENSUOufUtut+PTix4lwMgpu76a\n\tU0/iMUdV1BnC5O1Ums38O9xdcj23F+qUDjQXwEHg34yF/CiSLw6oR7dw8EmcQqBiS/\n\tPLsrugdrqfvOlxABCjzqvMmM6boHqkyaauvy1vbE=","Content-Type":"text/plain; charset=\"utf-8\"","MIME-Version":"1.0","Content-Transfer-Encoding":"quoted-printable","In-Reply-To":"<175822087394.3990114.6026778704604652527@ping.linuxembedded.co.uk>","References":"<20250918144333.108695-1-stefan.klug@ideasonboard.com>\n\t<20250918144333.108695-19-stefan.klug@ideasonboard.com>\n\t<175822087394.3990114.6026778704604652527@ping.linuxembedded.co.uk>","Subject":"Re: [PATCH v4 18/19] ipa: rkisp1: Add WDR algorithm","From":"Stefan Klug <stefan.klug@ideasonboard.com>","Cc":"Paul Elder <paul.elder@ideasonboard.com>","To":"Kieran Bingham <kieran.bingham@ideasonboard.com>,\n\tlibcamera-devel@lists.libcamera.org","Date":"Fri, 19 Sep 2025 11:28:32 +0200","Message-ID":"<175827411258.17312.7120433553558011292@localhost>","User-Agent":"alot/0.12.dev8+g2c003385c862.d20250602","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>"}}]