[v2,06/12] ipa: libipa: Add GammaAlgorithm class
diff mbox series

Message ID 20260626-ipu3-libipa-rework-v2-6-41546e23de3e@ideasonboard.com
State New
Headers show
Series
  • libipa: Re-work IPU3 IPA to use libipa algorithms
Related show

Commit Message

Dan Scally June 26, 2026, 1:05 p.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.

Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
Signed-off-by: Daniel Scally <dan.scally@ideasonboard.com>
---
Changes in v2:

        - Updated documentation comments, and minor styling changes
---
 src/ipa/libipa/gamma.cpp   | 257 +++++++++++++++++++++++++++++++++++++++++++++
 src/ipa/libipa/gamma.h     |  94 +++++++++++++++++
 src/ipa/libipa/meson.build |   2 +
 3 files changed, 353 insertions(+)

Comments

Kieran Bingham June 29, 2026, 10:08 a.m. UTC | #1
Quoting Daniel Scally (2026-06-26 14:05:53)
> 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.
> 
> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>

I'm interested to see where this goes, but having a common block looks
useful so:


Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>


> Signed-off-by: Daniel Scally <dan.scally@ideasonboard.com>
> ---
> Changes in v2:
> 
>         - Updated documentation comments, and minor styling changes
> ---
>  src/ipa/libipa/gamma.cpp   | 257 +++++++++++++++++++++++++++++++++++++++++++++
>  src/ipa/libipa/gamma.h     |  94 +++++++++++++++++
>  src/ipa/libipa/meson.build |   2 +
>  3 files changed, 353 insertions(+)
> 
> diff --git a/src/ipa/libipa/gamma.cpp b/src/ipa/libipa/gamma.cpp
> new file mode 100644
> index 0000000000000000000000000000000000000000..5fec5329a50f0b3dd0e39504dfbb63484ca4fcd9
> --- /dev/null
> +++ b/src/ipa/libipa/gamma.cpp
> @@ -0,0 +1,257 @@
> +/* 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
> + */
> +
> +/**
> + * \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. The piecewise linear
> + * function will be applied on a number of knots whose position is described by
> + * the optional \a segments argument, which describes each segment's relative
> + * length.
> + *
> + * For example, if the gamma correction has to be applied on 16 equally spaced
> + * sampling points, a \segments array like:
> + *
> + * [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.
> + *;
> + * If an IPA implementation doesnt't provide \a segments, the GammaAlgorithm
> + * class consturcts an evenly-spaced default.
> + *
> + * 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 gamma LUT sampling points
> + */
> +
> +/**
> + * \var GammaAlgorithmBase::defaultGamma_
> + * \brief The default gamma parameter
> + */
> +
> +/**
> + * \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 gamma LUT sampling points
> + * \tparam UQ The fixedpoint representation 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.
> + *
> + * 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.
> + *
> + * Useful links:
> + * - https://www.cambridgeincolour.com/tutorials/gamma-correction.htm
> + * - https://en.wikipedia.org/wiki/SRGB
> + */
> +
> +/**
> + * \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..94be8c7a2328e7b5a3618b0d65989ee511cece09
> --- /dev/null
> +++ b/src/ipa/libipa/gamma.h
> @@ -0,0 +1,94 @@
> +/* 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',
> 
> -- 
> 2.43.0
>
Barnabás Pőcze June 30, 2026, 8:53 a.m. UTC | #2
2026. 06. 26. 15:05 keltezéssel, Daniel Scally írta:
> 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.
> 
> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> Signed-off-by: Daniel Scally <dan.scally@ideasonboard.com>
> ---
> Changes in v2:
> 
>          - Updated documentation comments, and minor styling changes
> ---
>   src/ipa/libipa/gamma.cpp   | 257 +++++++++++++++++++++++++++++++++++++++++++++
>   src/ipa/libipa/gamma.h     |  94 +++++++++++++++++
>   src/ipa/libipa/meson.build |   2 +
>   3 files changed, 353 insertions(+)
> 
> diff --git a/src/ipa/libipa/gamma.cpp b/src/ipa/libipa/gamma.cpp
> new file mode 100644
> index 0000000000000000000000000000000000000000..5fec5329a50f0b3dd0e39504dfbb63484ca4fcd9
> --- /dev/null
> +++ b/src/ipa/libipa/gamma.cpp
> @@ -0,0 +1,257 @@
> [...]
> +/**
> + * \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;

Is there not a risk of dropping frame 0? I suppose as long as the request
sequence number is used, this is guaranteed to work.


> +
> +	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;
> +}
> [...]
> diff --git a/src/ipa/libipa/gamma.h b/src/ipa/libipa/gamma.h
> new file mode 100644
> index 0000000000000000000000000000000000000000..94be8c7a2328e7b5a3618b0d65989ee511cece09
> --- /dev/null
> +++ b/src/ipa/libipa/gamma.h
> @@ -0,0 +1,94 @@
> +/* 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>

Should template arguments be always camel case? I suppose this is a question
for the style guide.


> +class GammaAlgorithm : public GammaAlgorithmBase
> +{
> +public:
> +	GammaAlgorithm()
> +		: GammaAlgorithmBase(nLutNodes)
> +	{
> +	}
> +
> +	template<typename T>
> +	void prepare(gamma::FrameContext &context, Span<T> lut)

This could be `Span<T, nLutNodes>` to enforce the size. Feels like a worthwhile
change to me, even if creating a spans becomes a bit bit more convoluted.


> +	{
> +		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 */
> [...]

Patch
diff mbox series

diff --git a/src/ipa/libipa/gamma.cpp b/src/ipa/libipa/gamma.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5fec5329a50f0b3dd0e39504dfbb63484ca4fcd9
--- /dev/null
+++ b/src/ipa/libipa/gamma.cpp
@@ -0,0 +1,257 @@ 
+/* 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
+ */
+
+/**
+ * \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. The piecewise linear
+ * function will be applied on a number of knots whose position is described by
+ * the optional \a segments argument, which describes each segment's relative
+ * length.
+ *
+ * For example, if the gamma correction has to be applied on 16 equally spaced
+ * sampling points, a \segments array like:
+ *
+ * [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.
+ *;
+ * If an IPA implementation doesnt't provide \a segments, the GammaAlgorithm
+ * class consturcts an evenly-spaced default.
+ *
+ * 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 gamma LUT sampling points
+ */
+
+/**
+ * \var GammaAlgorithmBase::defaultGamma_
+ * \brief The default gamma parameter
+ */
+
+/**
+ * \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 gamma LUT sampling points
+ * \tparam UQ The fixedpoint representation 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.
+ *
+ * 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.
+ *
+ * Useful links:
+ * - https://www.cambridgeincolour.com/tutorials/gamma-correction.htm
+ * - https://en.wikipedia.org/wiki/SRGB
+ */
+
+/**
+ * \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..94be8c7a2328e7b5a3618b0d65989ee511cece09
--- /dev/null
+++ b/src/ipa/libipa/gamma.h
@@ -0,0 +1,94 @@ 
+/* 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',