[05/10] ipa: libipa: Add GammaAlgorithm class
diff mbox series

Message ID 20260616-ipu3-libipa-rework-v1-5-d4448b54f1d8@ideasonboard.com
State Superseded
Headers show
Series
  • libipa: Re-work IPU3 IPA to use libipa algorithms
Related show

Commit Message

Dan Scally June 16, 2026, 6:41 a.m. UTC
Add a base GammaAlgorithm class that can be used by IPA specific
gamma algorithms to reduce the amount of work that they need to
implement.

Signed-off-by: Daniel Scally <dan.scally@ideasonboard.com>
---
 src/ipa/libipa/gamma.cpp   | 248 +++++++++++++++++++++++++++++++++++++++++++++
 src/ipa/libipa/gamma.h     |  93 +++++++++++++++++
 src/ipa/libipa/meson.build |   2 +
 3 files changed, 343 insertions(+)

Comments

Jacopo Mondi June 17, 2026, 9:30 a.m. UTC | #1
Hi Dan

On Tue, Jun 16, 2026 at 07:41:39AM +0100, Daniel Scally wrote:
> Add a base GammaAlgorithm class that can be used by IPA specific
> gamma algorithms to reduce the amount of work that they need to
> implement.
>
> Signed-off-by: Daniel Scally <dan.scally@ideasonboard.com>
> ---
>  src/ipa/libipa/gamma.cpp   | 248 +++++++++++++++++++++++++++++++++++++++++++++
>  src/ipa/libipa/gamma.h     |  93 +++++++++++++++++
>  src/ipa/libipa/meson.build |   2 +
>  3 files changed, 343 insertions(+)
>
> diff --git a/src/ipa/libipa/gamma.cpp b/src/ipa/libipa/gamma.cpp
> new file mode 100644
> index 0000000000000000000000000000000000000000..f84f4cf7049f695f6a4ea21e0bcf851aef0f2c70
> --- /dev/null
> +++ b/src/ipa/libipa/gamma.cpp
> @@ -0,0 +1,248 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026 Ideas on Board Oy
> + *
> + * libIPA Gamma correction algorithm
> + */
> +
> +#include "gamma.h"
> +
> +#include <numeric>
> +
> +#include <libcamera/controls.h>
> +
> +#include "libcamera/internal/value_node.h"
> +
> +/**
> + * \file gamma.h
> + * \brief libipa implementation of a gamma curve correction algorithm
> + */
> +
> +namespace libcamera {
> +
> +namespace ipa {
> +
> +LOG_DEFINE_CATEGORY(Gamma)
> +
> +namespace gamma {
> +
> +/**
> + * \struct ActiveState
> + * \brief Active gamma correction algorithm state
> + *
> + * \var ActiveState::gamma
> + * \brief The gamma correction value applied as 1.0/gamma
> + */
> +
> +/**
> + * \struct FrameContext
> + * \brief Per-frame gamma correction settings
> + *
> + * \var FrameContext::gamma
> + * \brief The gamma correction value applied for this frame

If you want to specify 1.0/gamma in the documentation of
ActiveState::gamma please do the same here. Personally I wouldn't
mention 1.0/gamma as it comes by the definition of what a compression gamma
curve is.

> + *
> + * \var FrameContext::update
> + * \brief A flag instructing the algorithm to push an update to the hardware
> + */
> +
> +} /* namespace gamma */
> +
> +/**
> + * \brief The default gamma correction value
> + */
> +const float kDefaultGamma = 2.2f;
> +
> +/**
> + * \class GammaAlgorithmBase
> + * \brief Base class for GammaAlgorithm to implement non-templated functions
> + *
> + * This base class for GammeaAlgorithm allows us to implement non templated
> + * functions. IPA specific implementations shall derive from GammaAlgorithm and
> + * not this class.
> + */
> +
> +/**
> + * \fn GammaAlgorithmBase::GammaAlgorithmBase
> + * \brief Construct an instance of the class
> + * \param[in] nLutNodes Set the number of function knee-points expected by the
> + * IPA algorithm
> + */
> +
> +/**
> + * \brief Initialise the algorithm with the given tuning data
> + * \param[out] controls The ControlList into which this algorithm's supported
> + * controls will be emplaced.
> + * \param[in] tuningData The tuning data to use with the algorithm
> + * \param[in] segments	A vector of segment spacings to define a custom

Is this weirdly spaced or does it only shows up this way in the diff ?

> + * X coordinate system for the curve
> + *
> + * Parse \a tuningData and \a segments to initialize the gamma correction curve.
> + * The tuning data may contain a default gamma value to use; otherwise the value
> + * of \a kDefaultGamma will be taken as the default. \a segments may provide a

Before describing the segmentation methods, I think we should tell
that gamma is implemented as a pwl applied on a list of sampling
points. We should re-use (a slightly reworked version of) the
documentation from the rkisp1 implementation (remember to remove it in
the next patches from rkisp1)

 * This algorithm implements gamma out curve compression with a
 * gamma value specified in \a tuningData or with a default gamma
 * value of \a kDefaultGamma.
 *
 * Gamma correction is internally implemented as a piecewise linear function
 * applied on a number of knots whose position is described by the optional
 * \a segments argument.
 *
 * Useful links:
 * - https://www.cambridgeincolour.com/tutorials/gamma-correction.htm
 * - https://en.wikipedia.org/wiki/SRGB

> + * view into an array of segment spacings, which can be used to vary the X

Give the above introduction, this can be simplified as:

The optional \a segments argument defines the segmentation of the gamma
LUT sampling points by describing each segments relative length.

> + * co-ordinate of the gamma correction curve. For example, if the piecewise
> + * linear function of the correction curve is expected to have 16 knee-points, a

I think the key is that there are 16 -equally spaced- knee-points
here. Give the above introduction I would simplify this as

For example, if the gamma correction has to be applied on 16 equally
spaced sampling points, a \a segments array like so:

> + * \a segments array like so:
> + *
> + * [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
> + *
> + * would result in evenly spaced knee-points along the X-axis. Hardware may

Break the paragraph and insert an empty line after X-axis.

> + * expect the knee-points to be spaced more densely towards the start of the
> + * curve and more sparsely towards the end, in which case an alternative array
> + * might bev

"might be:"

> + *
> + * [1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 8, 8, 8, 8, 8]
> + *
> + * As the values in \a segments represent the distance between two knee-points
> + * relative to the total distance between the first and last point, the length
> + * of \a segments should be equal to the number of knee-points minus one. As a

Break the paragraph maybe ? Also

      If an IPA implementation doesn't provide \a segments, the
      GammaAlgorithm class constructs and evenly-spaced default one.

> + * convenience, a hardware-specific algorithm deriving from this class may omit
> + * \a segments, in which case an evenly-spaced default will be constructed.
> + *
> + * IPA modules are expected to call this function as part of their
> + * implementation of Algorithm::init()
> + *
> + * @return 0 on success, a negative error code otherwise
> + */
> +int GammaAlgorithmBase::init(ControlInfoMap::Map &controls, const ValueNode &tuningData,
> +			     Span<unsigned int> segments)
> +{
> +	/*
> +	 * If the caller doesn't pass in a segment list we simply construct one
> +	 * with equally spaced segments. We need one less segment than we have
> +	 * LUT nodes.
> +	 */
> +	unsigned int expectedNSegments = nLutNodes_ - 1;
> +
> +	if (segments.empty())
> +		for (unsigned int i = 0; i < expectedNSegments; i++)
> +			segments_.push_back(1);
> +	else
> +		segments_.assign(segments.begin(), segments.end());

for complex statements like this on I think we usually use {} (at
least in Linux)

        if () {
                for ()
                      ...
        } else {
                ...
        }

> +
> +	if (segments_.size() != expectedNSegments)
> +		return -EINVAL;
> +
> +	segmentsSum_ = std::accumulate(segments_.begin(), segments_.end(), 0.0f);
> +
> +	defaultGamma_ = tuningData["gamma"].get<double>(kDefaultGamma);
> +	controls[&controls::Gamma] = ControlInfo(0.1f, 10.0f, defaultGamma_);

should the max gamma value be derived from the UQ template argument ?

> +
> +	return 0;
> +}
> +
> +/**
> + * \brief Configure the gamma correction algorithm
> + * \param[out] state The gamma correction algorithm's active state
> + *
> + * Reset to the default gamma correction value.
> + *
> + * IPA modules are expected to call this function as part of their
> + * implementation of Algorithm::configure()
> + */
> +void GammaAlgorithmBase::configure(gamma::ActiveState &state)
> +{
> +	state.gamma = defaultGamma_;
> +}
> +
> +/**
> + * \brief Queue a request to the gamma correction algorithm
> + * \param[in] state The algorithm's active state
> + * \param[in] frame The current frame number
> + * \param[in] context The algorithm's frame context
> + * \param[in] controls The ControlList that was queued with the request
> + *
> + * Queue a new request to the gamma correction algorithm and handle any relevant
> + * controls that were queued. The only control currently handled is:
> + *
> + * - controls::Gamma
> + *
> + * If a control with that ID is queued the value is stored in \a state and
> + * \a context.
> + *
> + * IPA modules are expected to call this function as part of their
> + * implementation of Algorithm::queueRequest()
> + */
> +void GammaAlgorithmBase::queueRequest(gamma::ActiveState &state,
> +				      const uint32_t frame,
> +				      gamma::FrameContext &context,
> +				      const ControlList &controls)
> +{
> +	if (frame == 0)
> +		context.update = true;
> +
> +	const auto &gamma = controls.get(controls::Gamma);
> +	if (gamma) {
> +		state.gamma = *gamma;
> +		context.update = true;
> +		LOG(Gamma, Info) << "Set gamma to " << *gamma;
> +	}
> +
> +	context.gamma = state.gamma;
> +}
> +
> +/**
> + * \brief Populate metadata with the gamma correction values for a frame
> + * \param[in] context The frame context
> + * \param[out] metadata The ControlList of metadata for a frame
> + *
> + * Report the gamma value used to calculate the correction curve that was
> + * applied to a frame.
> + */
> +void GammaAlgorithmBase::process(gamma::FrameContext &context, ControlList &metadata)
> +{
> +	metadata.set(controls::Gamma, context.gamma);
> +}
> +
> +/**
> + * \var GammaAlgorithmBase::nLutNodes_
> + * \brief The number of knee-points in the gamma correction curve
> + */
> +
> +/**
> + * \var GammaAlgorithmBase::defaultGamma_
> + * \brief The default gamma parameter used at stream start

s/used at stream start// ?

> + */
> +
> +/**
> + * \var GammaAlgorithmBase::segments_
> + * \brief The vector of segment sizes describing the space between knee-points
> + */
> +
> +/**
> + * \var GammaAlgorithmBase::segmentsSum_
> + * \brief The sum of \a GammaAlgorithmBase::segments_
> + */
> +
> +/**
> + * \class GammaAlgorithm
> + * \brief The libipa gamma correction algorithm
> + * \tparam nLutNodes The number of knee-points in the algorithm's function

 * \tparam nLutNodes The number of knee-points in the gamma correction
 * pwl approximation

if you want to use "knee points" ?
Or

 * \tparam nLutNodes The number of gamma LUT sampling points

?

> + * \tparam UQ The fixedpoint representation of the function's values

of the gamma correction values

> + *
> + * Gamma correction adjusts for the differences in the way light is perceived
> + * by a camera and the human eye by applying a function to the input values.
> + * The GammaAlgorithm class facilitates this by building a piecewise linear
> + * function from a gamma parameter and supplying it in the hardware-specific
> + * formats defined by the IPA algorithms.

Ah, that's the intro I was suggesting (in the wrong place most
probably). Could you see if any of what I suggested still applies
there to better clarify the usage of 'segments' ?

> + *
> + * IPA modules are expected to store an instance of GammaAlgorithm as a class
> + * member, templated with the format and number of knee-points in the PWL
> + * expected by their hardware and then call its functions in their overload of
> + * the Algorithm class's function.
> + *
> + * When an application queues a new value for the gamma parameter with a
> + * Request, the GammaAlgorithm will recalculate and populate the new LUT to be
> + * sent to the ISP.
> + */
> +
> +/**
> + * \fn GammaAlgorithm::prepare()
> + * \tparam T The type of data expected by the hardware's look-up table
> + * \param[in] context The frame context
> + * \param[out] lut The Span into which to place the calculated look-up table
> + */
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/gamma.h b/src/ipa/libipa/gamma.h
> new file mode 100644
> index 0000000000000000000000000000000000000000..2b449d9ec41bc96f576e664afca33acb8267a8e6
> --- /dev/null
> +++ b/src/ipa/libipa/gamma.h
> @@ -0,0 +1,93 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026 Ideas on Board Oy
> + *
> + * libIPA Gamma correction algorithm
> + */
> +
> +#pragma once
> +
> +#include <cmath>
> +#include <vector>
> +
> +#include <libcamera/base/log.h>
> +#include <libcamera/base/span.h>
> +
> +#include <libcamera/control_ids.h>
> +
> +#include "libcamera/internal/value_node.h"
> +
> +#include "fixedpoint.h"
> +
> +namespace libcamera {
> +
> +namespace ipa {
> +
> +LOG_DECLARE_CATEGORY(Gamma)
> +
> +namespace gamma {
> +
> +struct ActiveState {
> +	double gamma;
> +};
> +
> +struct FrameContext {
> +	double gamma;
> +	bool update;
> +};
> +
> +} /* namespace gamma */
> +
> +class GammaAlgorithmBase
> +{
> +public:
> +	GammaAlgorithmBase(unsigned int nLutNodes)
> +		: nLutNodes_(nLutNodes)
> +	{
> +	}
> +
> +	int init(ControlInfoMap::Map &controls, const ValueNode &tuningData,
> +		 Span<unsigned int> segments = {});
> +
> +	void configure(gamma::ActiveState &state);
> +	void queueRequest(gamma::ActiveState &state, const uint32_t frame,
> +			  gamma::FrameContext &context, const ControlList &controls);
> +	void process(gamma::FrameContext &context, ControlList &metadata);
> +
> +protected:
> +	unsigned int nLutNodes_;
> +	float defaultGamma_;
> +	std::vector<unsigned int> segments_;
> +	unsigned int segmentsSum_;
> +};
> +
> +template<unsigned int nLutNodes, typename UQ>
> +class GammaAlgorithm : public GammaAlgorithmBase
> +{
> +public:
> +	GammaAlgorithm()
> +		: GammaAlgorithmBase(nLutNodes)
> +	{
> +	}
> +
> +	template<typename T>
> +	void prepare(gamma::FrameContext &context, Span<T> lut)
> +	{
> +		float x = 0;
> +
> +		for (unsigned int i = 0; i < nLutNodes_; i++) {
> +			float gamma = std::pow(x / segmentsSum_,
> +					       1.0 / context.gamma);
> +			lut[i] = UQ(gamma).quantized();
> +
> +			LOG(Gamma, Debug) << "LUT[" << i << "]=" << gamma << "(" << lut[i] << ")";

I bet you can easily break this to multiple lines.

With documentation adjusted:
Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>

Thanks
  j

> +
> +			if (i < segments_.size())
> +				x += segments_[i];
> +		}
> +	}
> +};
> +
> +} /* namespace ipa */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build
> index da6ea0c5e13000f78b2196c7334610c350f1ad13..565da9be9059f2167d107061d643e58202e655ef 100644
> --- a/src/ipa/libipa/meson.build
> +++ b/src/ipa/libipa/meson.build
> @@ -12,6 +12,7 @@ libipa_headers = files([
>      'exposure_mode_helper.h',
>      'fc_queue.h',
>      'fixedpoint.h',
> +    'gamma.h',
>      'histogram.h',
>      'interpolator.h',
>      'lsc.h',
> @@ -37,6 +38,7 @@ libipa_sources = files([
>      'exposure_mode_helper.cpp',
>      'fc_queue.cpp',
>      'fixedpoint.cpp',
> +    'gamma.cpp',
>      'histogram.cpp',
>      'interpolator.cpp',
>      'lsc.cpp',
>
> --
> 2.43.0
>
Dan Scally June 19, 2026, 8:41 a.m. UTC | #2
Hi Jacopo - thanks for all the reviews

On 17/06/2026 10:30, Jacopo Mondi wrote:
> Hi Dan
> 
> On Tue, Jun 16, 2026 at 07:41:39AM +0100, Daniel Scally wrote:
>> Add a base GammaAlgorithm class that can be used by IPA specific
>> gamma algorithms to reduce the amount of work that they need to
>> implement.
>>
>> Signed-off-by: Daniel Scally <dan.scally@ideasonboard.com>
>> ---
>>   src/ipa/libipa/gamma.cpp   | 248 +++++++++++++++++++++++++++++++++++++++++++++
>>   src/ipa/libipa/gamma.h     |  93 +++++++++++++++++
>>   src/ipa/libipa/meson.build |   2 +
>>   3 files changed, 343 insertions(+)
>>
>> diff --git a/src/ipa/libipa/gamma.cpp b/src/ipa/libipa/gamma.cpp
>> new file mode 100644
>> index 0000000000000000000000000000000000000000..f84f4cf7049f695f6a4ea21e0bcf851aef0f2c70
>> --- /dev/null
>> +++ b/src/ipa/libipa/gamma.cpp
>> @@ -0,0 +1,248 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright (C) 2026 Ideas on Board Oy
>> + *
>> + * libIPA Gamma correction algorithm
>> + */
>> +
>> +#include "gamma.h"
>> +
>> +#include <numeric>
>> +
>> +#include <libcamera/controls.h>
>> +
>> +#include "libcamera/internal/value_node.h"
>> +
>> +/**
>> + * \file gamma.h
>> + * \brief libipa implementation of a gamma curve correction algorithm
>> + */
>> +
>> +namespace libcamera {
>> +
>> +namespace ipa {
>> +
>> +LOG_DEFINE_CATEGORY(Gamma)
>> +
>> +namespace gamma {
>> +
>> +/**
>> + * \struct ActiveState
>> + * \brief Active gamma correction algorithm state
>> + *
>> + * \var ActiveState::gamma
>> + * \brief The gamma correction value applied as 1.0/gamma
>> + */
>> +
>> +/**
>> + * \struct FrameContext
>> + * \brief Per-frame gamma correction settings
>> + *
>> + * \var FrameContext::gamma
>> + * \brief The gamma correction value applied for this frame
> 
> If you want to specify 1.0/gamma in the documentation of
> ActiveState::gamma please do the same here. Personally I wouldn't
> mention 1.0/gamma as it comes by the definition of what a compression gamma
> curve is.

Okedokey, I'll drop it from ActiveState::gamma
> 
>> + *
>> + * \var FrameContext::update
>> + * \brief A flag instructing the algorithm to push an update to the hardware
>> + */
>> +
>> +} /* namespace gamma */
>> +
>> +/**
>> + * \brief The default gamma correction value
>> + */
>> +const float kDefaultGamma = 2.2f;
>> +
>> +/**
>> + * \class GammaAlgorithmBase
>> + * \brief Base class for GammaAlgorithm to implement non-templated functions
>> + *
>> + * This base class for GammeaAlgorithm allows us to implement non templated
>> + * functions. IPA specific implementations shall derive from GammaAlgorithm and
>> + * not this class.
>> + */
>> +
>> +/**
>> + * \fn GammaAlgorithmBase::GammaAlgorithmBase
>> + * \brief Construct an instance of the class
>> + * \param[in] nLutNodes Set the number of function knee-points expected by the
>> + * IPA algorithm
>> + */
>> +
>> +/**
>> + * \brief Initialise the algorithm with the given tuning data
>> + * \param[out] controls The ControlList into which this algorithm's supported
>> + * controls will be emplaced.
>> + * \param[in] tuningData The tuning data to use with the algorithm
>> + * \param[in] segments	A vector of segment spacings to define a custom
> 
> Is this weirdly spaced or does it only shows up this way in the diff ?

It's a tab! Good catch.
> 
>> + * X coordinate system for the curve
>> + *
>> + * Parse \a tuningData and \a segments to initialize the gamma correction curve.
>> + * The tuning data may contain a default gamma value to use; otherwise the value
>> + * of \a kDefaultGamma will be taken as the default. \a segments may provide a
> 
> Before describing the segmentation methods, I think we should tell
> that gamma is implemented as a pwl applied on a list of sampling
> points. We should re-use (a slightly reworked version of) the
> documentation from the rkisp1 implementation (remember to remove it in
> the next patches from rkisp1)

You're right that this could do with expanding even though the comment on 
GammaAlgorithm::GammaAlgorithm() has the intro, I'll work on it and take in the suggestions below 
where I can.

> 
>   * This algorithm implements gamma out curve compression with a
>   * gamma value specified in \a tuningData or with a default gamma
>   * value of \a kDefaultGamma.
>   *
>   * Gamma correction is internally implemented as a piecewise linear function
>   * applied on a number of knots whose position is described by the optional
>   * \a segments argument.
>   *
>   * Useful links:
>   * - https://www.cambridgeincolour.com/tutorials/gamma-correction.htm
>   * - https://en.wikipedia.org/wiki/SRGB
> 
>> + * view into an array of segment spacings, which can be used to vary the X
> 
> Give the above introduction, this can be simplified as:
> 
> The optional \a segments argument defines the segmentation of the gamma
> LUT sampling points by describing each segments relative length.
> 
>> + * co-ordinate of the gamma correction curve. For example, if the piecewise
>> + * linear function of the correction curve is expected to have 16 knee-points, a
> 
> I think the key is that there are 16 -equally spaced- knee-points
> here. Give the above introduction I would simplify this as
> 
> For example, if the gamma correction has to be applied on 16 equally
> spaced sampling points, a \a segments array like so:
> 
>> + * \a segments array like so:
>> + *
>> + * [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
>> + *
>> + * would result in evenly spaced knee-points along the X-axis. Hardware may
> 
> Break the paragraph and insert an empty line after X-axis.
> 
>> + * expect the knee-points to be spaced more densely towards the start of the
>> + * curve and more sparsely towards the end, in which case an alternative array
>> + * might bev
> 
> "might be:"
> 
>> + *
>> + * [1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 8, 8, 8, 8, 8]
>> + *
>> + * As the values in \a segments represent the distance between two knee-points
>> + * relative to the total distance between the first and last point, the length
>> + * of \a segments should be equal to the number of knee-points minus one. As a
> 
> Break the paragraph maybe ? Also
> 
>        If an IPA implementation doesn't provide \a segments, the
>        GammaAlgorithm class constructs and evenly-spaced default one.
> 
>> + * convenience, a hardware-specific algorithm deriving from this class may omit
>> + * \a segments, in which case an evenly-spaced default will be constructed.
>> + *
>> + * IPA modules are expected to call this function as part of their
>> + * implementation of Algorithm::init()
>> + *
>> + * @return 0 on success, a negative error code otherwise
>> + */
>> +int GammaAlgorithmBase::init(ControlInfoMap::Map &controls, const ValueNode &tuningData,
>> +			     Span<unsigned int> segments)
>> +{
>> +	/*
>> +	 * If the caller doesn't pass in a segment list we simply construct one
>> +	 * with equally spaced segments. We need one less segment than we have
>> +	 * LUT nodes.
>> +	 */
>> +	unsigned int expectedNSegments = nLutNodes_ - 1;
>> +
>> +	if (segments.empty())
>> +		for (unsigned int i = 0; i < expectedNSegments; i++)
>> +			segments_.push_back(1);
>> +	else
>> +		segments_.assign(segments.begin(), segments.end());
> 
> for complex statements like this on I think we usually use {} (at
> least in Linux)

Ack!
> 
>          if () {
>                  for ()
>                        ...
>          } else {
>                  ...
>          }
> 
>> +
>> +	if (segments_.size() != expectedNSegments)
>> +		return -EINVAL;
>> +
>> +	segmentsSum_ = std::accumulate(segments_.begin(), segments_.end(), 0.0f);
>> +
>> +	defaultGamma_ = tuningData["gamma"].get<double>(kDefaultGamma);
>> +	controls[&controls::Gamma] = ControlInfo(0.1f, 10.0f, defaultGamma_);
> 
> should the max gamma value be derived from the UQ template argument ?

No I don't think so. Because we use UQ<0, N> the UQ::TraitsType::max value wouldn't be appropriate 
because it will always be 1.0...the gamma parameter just controls the steepness of the curve from 
0.0 to 1.0 which comes closer and closer to being a right angle as you increase the parameter, but I 
think there's no theoretical "maximum", so whatever number we choose here is somewhat arbitrary.

In theory we could make use of the fact that the UQ<0, N> can only represent values within a certain 
precision, and make an assessment about the maximum gain that could be practically applied based on 
the ability of that format to represent the different points with different quantized values...but 
experimenting with that, the rkisp1's UQ<0, 10> format is sufficient to accommodate gamma parameters 
up to ~96.0 which I think makes no sense.

Perhaps we should just select and document a min and max, even if they're somewhat arbitrary? 
Plotting some curves to play around, even 10.0 looks a bit "why would anyone choose this setting" 
and I lean towards a lower maximum making more sense. A gamma of 1.0 is a straight line, which is 
perhaps a more reasonable minimum too, unless we want to allow an inverted curve?

Thanks
Dan

> 
>> +
>> +	return 0;
>> +}
>> +
>> +/**
>> + * \brief Configure the gamma correction algorithm
>> + * \param[out] state The gamma correction algorithm's active state
>> + *
>> + * Reset to the default gamma correction value.
>> + *
>> + * IPA modules are expected to call this function as part of their
>> + * implementation of Algorithm::configure()
>> + */
>> +void GammaAlgorithmBase::configure(gamma::ActiveState &state)
>> +{
>> +	state.gamma = defaultGamma_;
>> +}
>> +
>> +/**
>> + * \brief Queue a request to the gamma correction algorithm
>> + * \param[in] state The algorithm's active state
>> + * \param[in] frame The current frame number
>> + * \param[in] context The algorithm's frame context
>> + * \param[in] controls The ControlList that was queued with the request
>> + *
>> + * Queue a new request to the gamma correction algorithm and handle any relevant
>> + * controls that were queued. The only control currently handled is:
>> + *
>> + * - controls::Gamma
>> + *
>> + * If a control with that ID is queued the value is stored in \a state and
>> + * \a context.
>> + *
>> + * IPA modules are expected to call this function as part of their
>> + * implementation of Algorithm::queueRequest()
>> + */
>> +void GammaAlgorithmBase::queueRequest(gamma::ActiveState &state,
>> +				      const uint32_t frame,
>> +				      gamma::FrameContext &context,
>> +				      const ControlList &controls)
>> +{
>> +	if (frame == 0)
>> +		context.update = true;
>> +
>> +	const auto &gamma = controls.get(controls::Gamma);
>> +	if (gamma) {
>> +		state.gamma = *gamma;
>> +		context.update = true;
>> +		LOG(Gamma, Info) << "Set gamma to " << *gamma;
>> +	}
>> +
>> +	context.gamma = state.gamma;
>> +}
>> +
>> +/**
>> + * \brief Populate metadata with the gamma correction values for a frame
>> + * \param[in] context The frame context
>> + * \param[out] metadata The ControlList of metadata for a frame
>> + *
>> + * Report the gamma value used to calculate the correction curve that was
>> + * applied to a frame.
>> + */
>> +void GammaAlgorithmBase::process(gamma::FrameContext &context, ControlList &metadata)
>> +{
>> +	metadata.set(controls::Gamma, context.gamma);
>> +}
>> +
>> +/**
>> + * \var GammaAlgorithmBase::nLutNodes_
>> + * \brief The number of knee-points in the gamma correction curve
>> + */
>> +
>> +/**
>> + * \var GammaAlgorithmBase::defaultGamma_
>> + * \brief The default gamma parameter used at stream start
> 
> s/used at stream start// ?
> 
>> + */
>> +
>> +/**
>> + * \var GammaAlgorithmBase::segments_
>> + * \brief The vector of segment sizes describing the space between knee-points
>> + */
>> +
>> +/**
>> + * \var GammaAlgorithmBase::segmentsSum_
>> + * \brief The sum of \a GammaAlgorithmBase::segments_
>> + */
>> +
>> +/**
>> + * \class GammaAlgorithm
>> + * \brief The libipa gamma correction algorithm
>> + * \tparam nLutNodes The number of knee-points in the algorithm's function
> 
>   * \tparam nLutNodes The number of knee-points in the gamma correction
>   * pwl approximation
> 
> if you want to use "knee points" ?
> Or
> 
>   * \tparam nLutNodes The number of gamma LUT sampling points
> 
> ?
> 
>> + * \tparam UQ The fixedpoint representation of the function's values
> 
> of the gamma correction values
> 
>> + *
>> + * Gamma correction adjusts for the differences in the way light is perceived
>> + * by a camera and the human eye by applying a function to the input values.
>> + * The GammaAlgorithm class facilitates this by building a piecewise linear
>> + * function from a gamma parameter and supplying it in the hardware-specific
>> + * formats defined by the IPA algorithms.
> 
> Ah, that's the intro I was suggesting (in the wrong place most
> probably). Could you see if any of what I suggested still applies
> there to better clarify the usage of 'segments' ?
> 
>> + *
>> + * IPA modules are expected to store an instance of GammaAlgorithm as a class
>> + * member, templated with the format and number of knee-points in the PWL
>> + * expected by their hardware and then call its functions in their overload of
>> + * the Algorithm class's function.
>> + *
>> + * When an application queues a new value for the gamma parameter with a
>> + * Request, the GammaAlgorithm will recalculate and populate the new LUT to be
>> + * sent to the ISP.
>> + */
>> +
>> +/**
>> + * \fn GammaAlgorithm::prepare()
>> + * \tparam T The type of data expected by the hardware's look-up table
>> + * \param[in] context The frame context
>> + * \param[out] lut The Span into which to place the calculated look-up table
>> + */
>> +
>> +} /* namespace ipa */
>> +
>> +} /* namespace libcamera */
>> diff --git a/src/ipa/libipa/gamma.h b/src/ipa/libipa/gamma.h
>> new file mode 100644
>> index 0000000000000000000000000000000000000000..2b449d9ec41bc96f576e664afca33acb8267a8e6
>> --- /dev/null
>> +++ b/src/ipa/libipa/gamma.h
>> @@ -0,0 +1,93 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright (C) 2026 Ideas on Board Oy
>> + *
>> + * libIPA Gamma correction algorithm
>> + */
>> +
>> +#pragma once
>> +
>> +#include <cmath>
>> +#include <vector>
>> +
>> +#include <libcamera/base/log.h>
>> +#include <libcamera/base/span.h>
>> +
>> +#include <libcamera/control_ids.h>
>> +
>> +#include "libcamera/internal/value_node.h"
>> +
>> +#include "fixedpoint.h"
>> +
>> +namespace libcamera {
>> +
>> +namespace ipa {
>> +
>> +LOG_DECLARE_CATEGORY(Gamma)
>> +
>> +namespace gamma {
>> +
>> +struct ActiveState {
>> +	double gamma;
>> +};
>> +
>> +struct FrameContext {
>> +	double gamma;
>> +	bool update;
>> +};
>> +
>> +} /* namespace gamma */
>> +
>> +class GammaAlgorithmBase
>> +{
>> +public:
>> +	GammaAlgorithmBase(unsigned int nLutNodes)
>> +		: nLutNodes_(nLutNodes)
>> +	{
>> +	}
>> +
>> +	int init(ControlInfoMap::Map &controls, const ValueNode &tuningData,
>> +		 Span<unsigned int> segments = {});
>> +
>> +	void configure(gamma::ActiveState &state);
>> +	void queueRequest(gamma::ActiveState &state, const uint32_t frame,
>> +			  gamma::FrameContext &context, const ControlList &controls);
>> +	void process(gamma::FrameContext &context, ControlList &metadata);
>> +
>> +protected:
>> +	unsigned int nLutNodes_;
>> +	float defaultGamma_;
>> +	std::vector<unsigned int> segments_;
>> +	unsigned int segmentsSum_;
>> +};
>> +
>> +template<unsigned int nLutNodes, typename UQ>
>> +class GammaAlgorithm : public GammaAlgorithmBase
>> +{
>> +public:
>> +	GammaAlgorithm()
>> +		: GammaAlgorithmBase(nLutNodes)
>> +	{
>> +	}
>> +
>> +	template<typename T>
>> +	void prepare(gamma::FrameContext &context, Span<T> lut)
>> +	{
>> +		float x = 0;
>> +
>> +		for (unsigned int i = 0; i < nLutNodes_; i++) {
>> +			float gamma = std::pow(x / segmentsSum_,
>> +					       1.0 / context.gamma);
>> +			lut[i] = UQ(gamma).quantized();
>> +
>> +			LOG(Gamma, Debug) << "LUT[" << i << "]=" << gamma << "(" << lut[i] << ")";
> 
> I bet you can easily break this to multiple lines.
> 
> With documentation adjusted:
> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> 
> Thanks
>    j
> 
>> +
>> +			if (i < segments_.size())
>> +				x += segments_[i];
>> +		}
>> +	}
>> +};
>> +
>> +} /* namespace ipa */
>> +
>> +} /* namespace libcamera */
>> diff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build
>> index da6ea0c5e13000f78b2196c7334610c350f1ad13..565da9be9059f2167d107061d643e58202e655ef 100644
>> --- a/src/ipa/libipa/meson.build
>> +++ b/src/ipa/libipa/meson.build
>> @@ -12,6 +12,7 @@ libipa_headers = files([
>>       'exposure_mode_helper.h',
>>       'fc_queue.h',
>>       'fixedpoint.h',
>> +    'gamma.h',
>>       'histogram.h',
>>       'interpolator.h',
>>       'lsc.h',
>> @@ -37,6 +38,7 @@ libipa_sources = files([
>>       'exposure_mode_helper.cpp',
>>       'fc_queue.cpp',
>>       'fixedpoint.cpp',
>> +    'gamma.cpp',
>>       'histogram.cpp',
>>       'interpolator.cpp',
>>       'lsc.cpp',
>>
>> --
>> 2.43.0
>>

Patch
diff mbox series

diff --git a/src/ipa/libipa/gamma.cpp b/src/ipa/libipa/gamma.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f84f4cf7049f695f6a4ea21e0bcf851aef0f2c70
--- /dev/null
+++ b/src/ipa/libipa/gamma.cpp
@@ -0,0 +1,248 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Ideas on Board Oy
+ *
+ * libIPA Gamma correction algorithm
+ */
+
+#include "gamma.h"
+
+#include <numeric>
+
+#include <libcamera/controls.h>
+
+#include "libcamera/internal/value_node.h"
+
+/**
+ * \file gamma.h
+ * \brief libipa implementation of a gamma curve correction algorithm
+ */
+
+namespace libcamera {
+
+namespace ipa {
+
+LOG_DEFINE_CATEGORY(Gamma)
+
+namespace gamma {
+
+/**
+ * \struct ActiveState
+ * \brief Active gamma correction algorithm state
+ *
+ * \var ActiveState::gamma
+ * \brief The gamma correction value applied as 1.0/gamma
+ */
+
+/**
+ * \struct FrameContext
+ * \brief Per-frame gamma correction settings
+ *
+ * \var FrameContext::gamma
+ * \brief The gamma correction value applied for this frame
+ *
+ * \var FrameContext::update
+ * \brief A flag instructing the algorithm to push an update to the hardware
+ */
+
+} /* namespace gamma */
+
+/**
+ * \brief The default gamma correction value
+ */
+const float kDefaultGamma = 2.2f;
+
+/**
+ * \class GammaAlgorithmBase
+ * \brief Base class for GammaAlgorithm to implement non-templated functions
+ *
+ * This base class for GammeaAlgorithm allows us to implement non templated
+ * functions. IPA specific implementations shall derive from GammaAlgorithm and
+ * not this class.
+ */
+
+/**
+ * \fn GammaAlgorithmBase::GammaAlgorithmBase
+ * \brief Construct an instance of the class
+ * \param[in] nLutNodes Set the number of function knee-points expected by the
+ * IPA algorithm
+ */
+
+/**
+ * \brief Initialise the algorithm with the given tuning data
+ * \param[out] controls The ControlList into which this algorithm's supported
+ * controls will be emplaced.
+ * \param[in] tuningData The tuning data to use with the algorithm
+ * \param[in] segments	A vector of segment spacings to define a custom
+ * X coordinate system for the curve
+ *
+ * Parse \a tuningData and \a segments to initialize the gamma correction curve.
+ * The tuning data may contain a default gamma value to use; otherwise the value
+ * of \a kDefaultGamma will be taken as the default. \a segments may provide a
+ * view into an array of segment spacings, which can be used to vary the X
+ * co-ordinate of the gamma correction curve. For example, if the piecewise
+ * linear function of the correction curve is expected to have 16 knee-points, a
+ * \a segments array like so:
+ *
+ * [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
+ *
+ * would result in evenly spaced knee-points along the X-axis. Hardware may
+ * expect the knee-points to be spaced more densely towards the start of the
+ * curve and more sparsely towards the end, in which case an alternative array
+ * might be:
+ *
+ * [1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 8, 8, 8, 8, 8]
+ *
+ * As the values in \a segments represent the distance between two knee-points
+ * relative to the total distance between the first and last point, the length
+ * of \a segments should be equal to the number of knee-points minus one. As a
+ * convenience, a hardware-specific algorithm deriving from this class may omit
+ * \a segments, in which case an evenly-spaced default will be constructed.
+ *
+ * IPA modules are expected to call this function as part of their
+ * implementation of Algorithm::init()
+ *
+ * @return 0 on success, a negative error code otherwise
+ */
+int GammaAlgorithmBase::init(ControlInfoMap::Map &controls, const ValueNode &tuningData,
+			     Span<unsigned int> segments)
+{
+	/*
+	 * If the caller doesn't pass in a segment list we simply construct one
+	 * with equally spaced segments. We need one less segment than we have
+	 * LUT nodes.
+	 */
+	unsigned int expectedNSegments = nLutNodes_ - 1;
+
+	if (segments.empty())
+		for (unsigned int i = 0; i < expectedNSegments; i++)
+			segments_.push_back(1);
+	else
+		segments_.assign(segments.begin(), segments.end());
+
+	if (segments_.size() != expectedNSegments)
+		return -EINVAL;
+
+	segmentsSum_ = std::accumulate(segments_.begin(), segments_.end(), 0.0f);
+
+	defaultGamma_ = tuningData["gamma"].get<double>(kDefaultGamma);
+	controls[&controls::Gamma] = ControlInfo(0.1f, 10.0f, defaultGamma_);
+
+	return 0;
+}
+
+/**
+ * \brief Configure the gamma correction algorithm
+ * \param[out] state The gamma correction algorithm's active state
+ *
+ * Reset to the default gamma correction value.
+ *
+ * IPA modules are expected to call this function as part of their
+ * implementation of Algorithm::configure()
+ */
+void GammaAlgorithmBase::configure(gamma::ActiveState &state)
+{
+	state.gamma = defaultGamma_;
+}
+
+/**
+ * \brief Queue a request to the gamma correction algorithm
+ * \param[in] state The algorithm's active state
+ * \param[in] frame The current frame number
+ * \param[in] context The algorithm's frame context
+ * \param[in] controls The ControlList that was queued with the request
+ *
+ * Queue a new request to the gamma correction algorithm and handle any relevant
+ * controls that were queued. The only control currently handled is:
+ *
+ * - controls::Gamma
+ *
+ * If a control with that ID is queued the value is stored in \a state and
+ * \a context.
+ *
+ * IPA modules are expected to call this function as part of their
+ * implementation of Algorithm::queueRequest()
+ */
+void GammaAlgorithmBase::queueRequest(gamma::ActiveState &state,
+				      const uint32_t frame,
+				      gamma::FrameContext &context,
+				      const ControlList &controls)
+{
+	if (frame == 0)
+		context.update = true;
+
+	const auto &gamma = controls.get(controls::Gamma);
+	if (gamma) {
+		state.gamma = *gamma;
+		context.update = true;
+		LOG(Gamma, Info) << "Set gamma to " << *gamma;
+	}
+
+	context.gamma = state.gamma;
+}
+
+/**
+ * \brief Populate metadata with the gamma correction values for a frame
+ * \param[in] context The frame context
+ * \param[out] metadata The ControlList of metadata for a frame
+ *
+ * Report the gamma value used to calculate the correction curve that was
+ * applied to a frame.
+ */
+void GammaAlgorithmBase::process(gamma::FrameContext &context, ControlList &metadata)
+{
+	metadata.set(controls::Gamma, context.gamma);
+}
+
+/**
+ * \var GammaAlgorithmBase::nLutNodes_
+ * \brief The number of knee-points in the gamma correction curve
+ */
+
+/**
+ * \var GammaAlgorithmBase::defaultGamma_
+ * \brief The default gamma parameter used at stream start
+ */
+
+/**
+ * \var GammaAlgorithmBase::segments_
+ * \brief The vector of segment sizes describing the space between knee-points
+ */
+
+/**
+ * \var GammaAlgorithmBase::segmentsSum_
+ * \brief The sum of \a GammaAlgorithmBase::segments_
+ */
+
+/**
+ * \class GammaAlgorithm
+ * \brief The libipa gamma correction algorithm
+ * \tparam nLutNodes The number of knee-points in the algorithm's function
+ * \tparam UQ The fixedpoint representation of the function's values
+ *
+ * Gamma correction adjusts for the differences in the way light is perceived
+ * by a camera and the human eye by applying a function to the input values.
+ * The GammaAlgorithm class facilitates this by building a piecewise linear
+ * function from a gamma parameter and supplying it in the hardware-specific
+ * formats defined by the IPA algorithms.
+ *
+ * IPA modules are expected to store an instance of GammaAlgorithm as a class
+ * member, templated with the format and number of knee-points in the PWL
+ * expected by their hardware and then call its functions in their overload of
+ * the Algorithm class's function.
+ *
+ * When an application queues a new value for the gamma parameter with a
+ * Request, the GammaAlgorithm will recalculate and populate the new LUT to be
+ * sent to the ISP.
+ */
+
+/**
+ * \fn GammaAlgorithm::prepare()
+ * \tparam T The type of data expected by the hardware's look-up table
+ * \param[in] context The frame context
+ * \param[out] lut The Span into which to place the calculated look-up table
+ */
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/gamma.h b/src/ipa/libipa/gamma.h
new file mode 100644
index 0000000000000000000000000000000000000000..2b449d9ec41bc96f576e664afca33acb8267a8e6
--- /dev/null
+++ b/src/ipa/libipa/gamma.h
@@ -0,0 +1,93 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Ideas on Board Oy
+ *
+ * libIPA Gamma correction algorithm
+ */
+
+#pragma once
+
+#include <cmath>
+#include <vector>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/span.h>
+
+#include <libcamera/control_ids.h>
+
+#include "libcamera/internal/value_node.h"
+
+#include "fixedpoint.h"
+
+namespace libcamera {
+
+namespace ipa {
+
+LOG_DECLARE_CATEGORY(Gamma)
+
+namespace gamma {
+
+struct ActiveState {
+	double gamma;
+};
+
+struct FrameContext {
+	double gamma;
+	bool update;
+};
+
+} /* namespace gamma */
+
+class GammaAlgorithmBase
+{
+public:
+	GammaAlgorithmBase(unsigned int nLutNodes)
+		: nLutNodes_(nLutNodes)
+	{
+	}
+
+	int init(ControlInfoMap::Map &controls, const ValueNode &tuningData,
+		 Span<unsigned int> segments = {});
+
+	void configure(gamma::ActiveState &state);
+	void queueRequest(gamma::ActiveState &state, const uint32_t frame,
+			  gamma::FrameContext &context, const ControlList &controls);
+	void process(gamma::FrameContext &context, ControlList &metadata);
+
+protected:
+	unsigned int nLutNodes_;
+	float defaultGamma_;
+	std::vector<unsigned int> segments_;
+	unsigned int segmentsSum_;
+};
+
+template<unsigned int nLutNodes, typename UQ>
+class GammaAlgorithm : public GammaAlgorithmBase
+{
+public:
+	GammaAlgorithm()
+		: GammaAlgorithmBase(nLutNodes)
+	{
+	}
+
+	template<typename T>
+	void prepare(gamma::FrameContext &context, Span<T> lut)
+	{
+		float x = 0;
+
+		for (unsigned int i = 0; i < nLutNodes_; i++) {
+			float gamma = std::pow(x / segmentsSum_,
+					       1.0 / context.gamma);
+			lut[i] = UQ(gamma).quantized();
+
+			LOG(Gamma, Debug) << "LUT[" << i << "]=" << gamma << "(" << lut[i] << ")";
+
+			if (i < segments_.size())
+				x += segments_[i];
+		}
+	}
+};
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build
index da6ea0c5e13000f78b2196c7334610c350f1ad13..565da9be9059f2167d107061d643e58202e655ef 100644
--- a/src/ipa/libipa/meson.build
+++ b/src/ipa/libipa/meson.build
@@ -12,6 +12,7 @@  libipa_headers = files([
     'exposure_mode_helper.h',
     'fc_queue.h',
     'fixedpoint.h',
+    'gamma.h',
     'histogram.h',
     'interpolator.h',
     'lsc.h',
@@ -37,6 +38,7 @@  libipa_sources = files([
     'exposure_mode_helper.cpp',
     'fc_queue.cpp',
     'fixedpoint.cpp',
+    'gamma.cpp',
     'histogram.cpp',
     'interpolator.cpp',
     'lsc.cpp',