[v3,14/21] ipa: rppx1: agc: Add
diff mbox series

Message ID 20260918120949.191668-15-barnabas.pocze@ideasonboard.com
State New
Headers show
Series
  • libcamera: rcar-gen4 + rpp-x1
Related show

Commit Message

Barnabás Pőcze Sept. 18, 2026, 12:09 p.m. UTC
From: Jacopo Mondi <jacopo.mondi@ideasonboard.com>

Add the algorithm to the rppx1 ipa module based on the corresponding
algorithm in the rkisp1 ipa module.

Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
---
 src/ipa/rppx1/algorithms/agc.cpp     | 391 +++++++++++++++++++++++++++
 src/ipa/rppx1/algorithms/agc.h       |  47 ++++
 src/ipa/rppx1/algorithms/meson.build |   1 +
 src/ipa/rppx1/ipa_context.h          |  19 +-
 src/ipa/rppx1/params.h               |   4 +
 src/ipa/rppx1/rppx1.cpp              |  17 +-
 src/ipa/rppx1/stats.h                |   5 +
 7 files changed, 481 insertions(+), 3 deletions(-)
 create mode 100644 src/ipa/rppx1/algorithms/agc.cpp
 create mode 100644 src/ipa/rppx1/algorithms/agc.h

Patch
diff mbox series

diff --git a/src/ipa/rppx1/algorithms/agc.cpp b/src/ipa/rppx1/algorithms/agc.cpp
new file mode 100644
index 0000000000..fd782cea3d
--- /dev/null
+++ b/src/ipa/rppx1/algorithms/agc.cpp
@@ -0,0 +1,391 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas On Board
+ *
+ * AGC/AEC mean-based control algorithm
+ */
+
+#include "agc.h"
+
+#include <algorithm>
+#include <cmath>
+#include <iterator>
+#include <vector>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
+
+#include <libcamera/control_ids.h>
+#include <libcamera/geometry.h>
+
+#include <libcamera/ipa/core_ipa_interface.h>
+
+#include "libcamera/internal/value_node.h"
+
+#include "libipa/fixedpoint.h"
+#include "libipa/histogram.h"
+
+/**
+ * \file agc.h
+ */
+
+namespace libcamera {
+
+namespace ipa::rppx1::algorithms {
+
+using WindowWeightQ = UQ<1, 4>;
+using MeasureCoeffQ = UQ<1, 7>;
+
+LOG_DEFINE_CATEGORY(RppX1Agc)
+
+int Agc::parseMeteringModes(IPAContext &context, const ValueNode &tuningData)
+{
+	if (!tuningData.isDictionary())
+		LOG(RppX1Agc, Warning)
+			<< "'AeMeteringMode' parameter not found in tuning file";
+
+	for (const auto &[key, value] : tuningData.asDict()) {
+		auto it = controls::AeMeteringModeNameValueMap.find(key);
+		if (it == controls::AeMeteringModeNameValueMap.end()) {
+			LOG(RppX1Agc, Warning)
+				<< "Skipping unknown metering mode '" << key << "'";
+			continue;
+		}
+
+		auto weights = value.get<std::vector<float>>().value_or(utils::defopt);
+		if (weights.size() != RPPX1_HIST_WEIGHT_GRIDS_SIZE) {
+			LOG(RppX1Agc, Error)
+				<< "Invalid 'AeMeteringMode:" << key << "': "
+				<< "expected " << RPPX1_HIST_WEIGHT_GRIDS_SIZE << " elements, "
+				<< "got " << weights.size();
+			return -EINVAL;
+		}
+
+		for (const auto &x : weights) {
+			if (x < 0 || x > 1) {
+				LOG(RppX1Agc, Error)
+					<< "Invalid 'AeMeteringMode:" << key << "' values: "
+					<< "elements must be in [0;1]";
+				return -EINVAL;
+			}
+		}
+
+		meteringModes_.try_emplace(it->second, std::move(weights));
+	}
+
+	if (meteringModes_.empty()) {
+		LOG(RppX1Agc, Warning)
+			<< "No metering modes read from tuning file; defaulting to matrix";
+
+		meteringModes_.try_emplace(
+			controls::MeteringMatrix,
+			RPPX1_HIST_WEIGHT_GRIDS_SIZE, 1.0f);
+	}
+
+	std::vector<ControlValue> meteringModes;
+	std::vector<int> meteringModeKeys = utils::map_keys(meteringModes_);
+	std::transform(meteringModeKeys.begin(), meteringModeKeys.end(),
+		       std::back_inserter(meteringModes),
+		       [](int x) { return ControlValue(x); });
+	context.ctrlMap[&controls::AeMeteringMode] = ControlInfo(meteringModes);
+
+	return 0;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::init
+ */
+int Agc::init(IPAContext &context, const ValueNode &tuningData)
+{
+	int ret;
+
+	ret = agc_.init(tuningData, context.camHelper.get(), {
+		.sensorInfo = context.sensorInfo,
+		.sensorControls = context.sensorControls,
+		.ctrlMap = context.ctrlMap,
+	});
+	if (ret)
+		return ret;
+
+	const ValueNode &meteringModes = tuningData["AeMeteringMode"];
+	ret = parseMeteringModes(context, meteringModes);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::configure
+ */
+int Agc::configure(IPAContext &context, const IPACameraSensorInfo &configInfo)
+{
+	int ret = agc_.configure(context.configuration.agc, context.activeState.agc, {
+		.sensorInfo = context.sensorInfo,
+		.sensorControls = context.sensorControls,
+		.ctrlMap = context.ctrlMap,
+	});
+	if (ret)
+		return ret;
+
+	context.activeState.agc.meteringMode =
+		static_cast<controls::AeMeteringModeEnum>(meteringModes_.begin()->first);
+
+	context.configuration.agc.measureWindow.h_offs = 0;
+	context.configuration.agc.measureWindow.v_offs = 0;
+	context.configuration.agc.measureWindow.h_size = configInfo.outputSize.width;
+	context.configuration.agc.measureWindow.v_size = configInfo.outputSize.height;
+
+	return 0;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::queueRequest
+ */
+void Agc::queueRequest(IPAContext &context,
+		       [[maybe_unused]] const uint32_t frame,
+		       IPAFrameContext &frameContext,
+		       const ControlList &controls)
+{
+	auto &agc = context.activeState.agc;
+
+	agc_.queueRequest(context.configuration.agc, agc, frameContext.agc, controls);
+
+	const auto &meteringMode = controls.get(controls::AeMeteringMode);
+	if (meteringMode) {
+		frameContext.agc.updateMetering = agc.meteringMode != *meteringMode;
+		agc.meteringMode =
+			static_cast<controls::AeMeteringModeEnum>(*meteringMode);
+	}
+	frameContext.agc.meteringMode = agc.meteringMode;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::prepare
+ */
+void Agc::prepare(IPAContext &context, const uint32_t frame,
+		  IPAFrameContext &frameContext, RppX1Params *params)
+{
+	agc_.prepare(context.activeState.agc, frameContext.agc);
+
+	if (frame > 0 && !frameContext.agc.updateMetering)
+		return;
+
+	/*
+	 * Configure the AEC measurements. Set the window, measure
+	 * continuously, and estimate Y as (R + G + B) x (85/256).
+	 */
+	auto exmConfig = params->block<BlockType::ExmPre1>();
+	exmConfig.setEnabled(true);
+
+	/* Jacopo:
+	 * we decided to let the driver calculate the sub-window sizes.
+	 *
+	 * I'm under the impression that all the rounding that happens in the
+	 * driver restrict the window enough to work with the limitation of
+	 * of measuring after denoise (Note 4 page 159).
+	 */
+
+	exmConfig->wnd = context.configuration.agc.measureWindow;
+	exmConfig->mode = RPPX1_EXP_MEASURING_MODE_BAYER;
+
+	/*
+	 * Jacopo
+	 *
+	 * RPPX1: set also channel (after denoise) and gains (x1.0) they were
+	 * harcoded in the driver.
+	 *
+	 * last_line can be programmed but the driver does that.
+	 */
+	exmConfig->channel_sel = RPPX1_MEAS_CHAN_SEL6;
+	exmConfig->coeff_r = MeasureCoeffQ(1.f).quantized();
+	exmConfig->coeff_g_gr = MeasureCoeffQ(1.f).quantized();
+	exmConfig->coeff_b = MeasureCoeffQ(1.f).quantized();
+	exmConfig->coeff_gb = MeasureCoeffQ(1.f).quantized(); /* Unused in RGB mode. */
+
+	auto hst = params->block<BlockType::HistPost>();
+	hst.setEnabled(true);
+
+	/*
+	 * Configure the histogram measurement. Set the window, produce a
+	 * luminance histogram, and set the weights and predivider.
+	 *
+	 * RPP-X1: we decided to let the driver calculate the sub-window sizes..
+	 * Also, we can program last_line, but the driver already does that
+	 */
+	hst->wnd = context.configuration.agc.measureWindow;
+
+	/*
+	 * In RGB mode, a linear combination of the RGB components is used
+	 * for the histogram. Use the BT.601 RGB -> Y coefficients.
+	 *
+	 * \todo is this OK?!
+	 */
+	hst->mode = RPPX1_HIST_MODE_RGB_COMBINED;
+	hst->coeff[0] = MeasureCoeffQ(0.299f).quantized();
+	hst->coeff[1] = MeasureCoeffQ(0.587f).quantized();
+	hst->coeff[2] = MeasureCoeffQ(0.114f).quantized();
+
+	/*
+	 * Jacopo:
+	 *
+	 * RPP-X1: we also have to select post-debayer as the histogram tap
+	 * point as we want to operate on RGB data.
+	 * It was hardcoded in the kernel driver.
+	 */
+	hst->channel_sel = RPPX1_MEAS_CHAN_SEL7;
+
+	std::span<const float> weights = meteringModes_.at(frameContext.agc.meteringMode);
+	ASSERT(weights.size() == std::size(hst->weights));
+	for (size_t i = 0; i < weights.size(); i++) {
+		WindowWeightQ w = weights[i];
+		hst->weights[i] = w.quantized();
+
+		LOG(RppX1Agc, Debug)
+			<< "weights[" << i << "]: " << w;
+	}
+
+	/*
+	 * Each bin has a 16-bit integral component. So the maximum number in it
+	 * is 65535. The worst case is when all pixels are in the same bin.
+	 *
+	 * To ensure no overflow even in that case, one pixel may be sampled out
+	 * of every `skip` pixel.
+	 */
+	constexpr uint32_t kBinMax = (1u << 16) - 1;
+	const double skip = double(hst->wnd.h_size * hst->wnd.v_size) / kBinMax;
+
+	/*
+	 * There is a separate vertical/horizontal step setting,
+	 * but let's try to find something square-ish.
+	 *
+	 * Subsampling by `prediv` vertically and horizontally should
+	 * ensure that no more that `kBinMax` pixels are sampled.
+	 *
+	 * \todo Take weights into account?
+	 */
+	const auto prediv = std::clamp<uint8_t>(std::ceil(std::sqrt(skip)), 1, 127);
+	hst->v_stepsize = prediv;
+	hst->h_step_inc = 65536 / prediv;
+
+	/* RPP-X1: sample shift/offset to 0 */
+	hst->sample_offs = 0;
+	hst->sample_shift = 0;
+
+	LOG(RppX1Agc, Debug)
+		<< "window: " << Rectangle(hst->wnd.h_offs, hst->wnd.v_offs, hst->wnd.h_size, hst->wnd.v_size) << ", "
+		<< "v_stepsize: " << hst->v_stepsize << ", "
+		<< "h_step_inc: " << hst->h_step_inc;
+}
+
+namespace {
+
+class AgcTraits final : public AgcMeanLuminance::Traits
+{
+public:
+	AgcTraits(std::span<const uint32_t> expMeans, std::span<const float> weights)
+		: expMeans_(expMeans), weights_(weights)
+	{
+		ASSERT(expMeans_.size() == weights_.size());
+	}
+
+	/**
+	 * \brief Estimate the relative luminance of the frame with a given gain
+	 * \param[in] gain The gain to apply to the frame
+	 *
+	 * This function estimates the average relative luminance of the frame that
+	 * would be output by the sensor if an additional \a gain was applied.
+	 *
+	 * The estimation is based on the AE statistics for the current frame. Y
+	 * averages for all cells are first multiplied by the gain, and then saturated
+	 * to approximate the sensor behaviour at high brightness values. The
+	 * approximation is quite rough, as it doesn't take into account non-linearities
+	 * when approaching saturation. In this case, saturating after the conversion to
+	 * YUV doesn't take into account the fact that the R, G and B components
+	 * contribute differently to the relative luminance.
+	 *
+	 * The values are normalized to the [0.0, 1.0] range, where 1.0 corresponds to a
+	 * theoretical perfect reflector of 100% reference white.
+	 *
+	 * More detailed information can be found in:
+	 * https://en.wikipedia.org/wiki/Relative_luminance
+	 *
+	 * \return The relative luminance
+	 */
+	double estimateLuminance(double gain) const override
+	{
+		constexpr double kMaxExpMean = (1u << 20) - 1;
+
+		double ySum = 0.0;
+		double wSum = 0.0;
+
+		/* Sum the averages, saturated to 255. */
+		for (unsigned i = 0; i < expMeans_.size(); i++) {
+			double w = weights_[i];
+			ySum += std::min<double>(expMeans_[i] * gain, kMaxExpMean) * w;
+			wSum += w;
+		}
+
+		/* \todo Weight with the AWB gains */
+
+		return ySum / wSum / kMaxExpMean;
+	}
+
+private:
+	std::span<const uint32_t> expMeans_;
+	std::span<const float> weights_;
+};
+
+} /* namespace */
+
+/**
+ * \brief Process RppX1 statistics, and run AGC operations
+ * \param[in] context The shared IPA context
+ * \param[in] frame The frame context sequence number
+ * \param[in] frameContext The current frame context
+ * \param[in] stats The RPP-X1 statistics and ISP results
+ * \param[out] metadata Metadata for the frame, to be filled by the algorithm
+ *
+ * Identify the current image brightness, and use that to estimate the optimal
+ * new exposure and gain for the scene.
+ */
+void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
+		  IPAFrameContext &frameContext, const RppX1Stats *stats,
+		  ControlList &metadata)
+{
+	const auto histPost = stats->block<StatsType::HistPost>();
+	const auto exmPre1 = stats->block<StatsType::ExmPre1>();
+
+	if (!histPost)
+		LOG(RppX1Agc, Error) << "HIST_POST data is missing in statistics";
+	if (!exmPre1)
+		LOG(RppX1Agc, Error) << "EXM_PRE1 data is missing in statistics";
+
+	if (histPost && exmPre1) {
+		static_assert(RPPX1_EXM_NUM_WIN == RPPX1_HIST_WEIGHT_GRIDS_SIZE);
+
+		agc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {{
+			.traits = AgcTraits{
+				exmPre1->exp_mean,
+				meteringModes_.at(frameContext.agc.meteringMode),
+			},
+			.yHist = {
+				/* The lower 4 bits are fractional and meant to be discarded. */
+				histPost->hist_bins,
+				[](uint32_t x) { return x >> 4; },
+			},
+			.exposure = frameContext.sensor.exposure,
+			.gain = frameContext.sensor.gain,
+		}}, metadata);
+	} else {
+		agc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {}, metadata);
+	}
+
+	metadata.set(controls::AeMeteringMode, frameContext.agc.meteringMode);
+}
+
+REGISTER_IPA_ALGORITHM(Agc, "Agc")
+
+} /* namespace ipa::rppx1::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rppx1/algorithms/agc.h b/src/ipa/rppx1/algorithms/agc.h
new file mode 100644
index 0000000000..1b3c55681a
--- /dev/null
+++ b/src/ipa/rppx1/algorithms/agc.h
@@ -0,0 +1,47 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas On Board
+ *
+ * Rpp-X1 AGC/AEC algorithm
+ */
+
+#pragma once
+
+#include <map>
+#include <vector>
+
+#include "libipa/agc.h"
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::rppx1::algorithms {
+
+class Agc : public Algorithm
+{
+public:
+	int init(IPAContext &context, const ValueNode &tuningData) override;
+	int configure(IPAContext &context, const IPACameraSensorInfo &configInfo) override;
+	void queueRequest(IPAContext &context,
+			  const uint32_t frame,
+			  IPAFrameContext &frameContext,
+			  const ControlList &controls) override;
+	void prepare(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     RppX1Params *params) override;
+	void process(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     const RppX1Stats *stats,
+		     ControlList &metadata) override;
+
+private:
+	int parseMeteringModes(IPAContext &context, const ValueNode &tuningData);
+
+	std::map<int32_t, std::vector<float>> meteringModes_;
+	AgcAlgorithm agc_;
+};
+
+} /* namespace ipa::rppx1::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rppx1/algorithms/meson.build b/src/ipa/rppx1/algorithms/meson.build
index 165f0ce054..394ea9510b 100644
--- a/src/ipa/rppx1/algorithms/meson.build
+++ b/src/ipa/rppx1/algorithms/meson.build
@@ -1,5 +1,6 @@ 
 # SPDX-License-Identifier: CC0-1.0
 
 rppx1_ipa_algorithms = files([
+    'agc.cpp',
     'blc.cpp',
 ])
diff --git a/src/ipa/rppx1/ipa_context.h b/src/ipa/rppx1/ipa_context.h
index 079fcab782..f6408e73bb 100644
--- a/src/ipa/rppx1/ipa_context.h
+++ b/src/ipa/rppx1/ipa_context.h
@@ -9,14 +9,14 @@ 
 
 #include <memory>
 
-#include <libcamera/base/utils.h>
+#include <linux/media/dreamchip/rppx1-config.h>
 
 #include <libcamera/control_ids.h>
 #include <libcamera/controls.h>
-#include <libcamera/geometry.h>
 
 #include <libcamera/ipa/core_ipa_interface.h>
 
+#include <libipa/agc.h>
 #include <libipa/camera_sensor_helper.h>
 #include <libipa/fc_queue.h>
 
@@ -25,12 +25,27 @@  namespace libcamera {
 namespace ipa::rppx1 {
 
 struct IPASessionConfiguration {
+	struct Agc : ipa::agc::Session {
+		rppx1_window measureWindow;
+	} agc;
 };
 
 struct IPAActiveState {
+	struct Agc : ipa::agc::ActiveState {
+		controls::AeMeteringModeEnum meteringMode;
+	} agc;
 };
 
 struct IPAFrameContext : public FrameContext {
+	struct {
+		uint32_t exposure;
+		double gain;
+	} sensor;
+
+	struct Agc : ipa::agc::FrameContext {
+		controls::AeMeteringModeEnum meteringMode;
+		bool updateMetering;
+	} agc;
 };
 
 struct IPAContext {
diff --git a/src/ipa/rppx1/params.h b/src/ipa/rppx1/params.h
index 05db309914..45adf43613 100644
--- a/src/ipa/rppx1/params.h
+++ b/src/ipa/rppx1/params.h
@@ -17,6 +17,8 @@  namespace ipa::rppx1 {
 
 enum class BlockType : uint16_t {
 	BlsPre1,
+	ExmPre1,
+	HistPost,
 };
 
 namespace details {
@@ -34,6 +36,8 @@  struct block_type {
 	};
 
 RPPX1_DEFINE_BLOCK_TYPE(BlsPre1, bls, BLS_PRE1)
+RPPX1_DEFINE_BLOCK_TYPE(ExmPre1, exm, EXM_PRE1)
+RPPX1_DEFINE_BLOCK_TYPE(HistPost, hist, HIST_POST)
 
 struct params_traits {
 	using id_type = BlockType;
diff --git a/src/ipa/rppx1/rppx1.cpp b/src/ipa/rppx1/rppx1.cpp
index 23ab8391b3..78c9370b29 100644
--- a/src/ipa/rppx1/rppx1.cpp
+++ b/src/ipa/rppx1/rppx1.cpp
@@ -27,6 +27,8 @@ 
 #include "libcamera/internal/mapped_framebuffer.h"
 #include "libcamera/internal/yaml_parser.h"
 
+#include <libipa/agc.h>
+
 #include "algorithms/algorithm.h"
 
 #include "ipa_context.h"
@@ -229,6 +231,9 @@  void IPARppX1::processStats(const uint32_t frame, const uint32_t bufferId,
 	IPAFrameContext &frameContext = context_.frameContexts.get(frame);
 	ControlList metadata(controls::controls);
 
+	std::tie(frameContext.sensor.exposure, frameContext.sensor.gain) =
+		agc::extractControls(sensorControls, context_.camHelper.get());
+
 	for (auto const &a : algorithms()) {
 		Algorithm *algo = static_cast<Algorithm *>(a.get());
 		algo->process(context_, frame, frameContext, &stats, metadata);
@@ -249,9 +254,19 @@  void IPARppX1::updateControls(ControlInfoMap *ipaControls)
 
 void IPARppX1::setControls(unsigned int frame)
 {
-	[[maybe_unused]] IPAFrameContext &frameContext = context_.frameContexts.get(frame);
+	IPAFrameContext &frameContext = context_.frameContexts.get(frame);
+
+	uint32_t exposure = frameContext.agc.exposure;
+	uint32_t vblank = frameContext.agc.vblank;
+
+	LOG(IPARppX1, Debug)
+		<< "Set controls for frame " << frame << ": exposure " << exposure
+		<< ", gain " << frameContext.agc.gain << ", vblank " << vblank;
 
 	ControlList ctrls(context_.sensorControls);
+	agc::prepareControls(ctrls, context_.camHelper.get(),
+			     exposure, frameContext.agc.gain);
+	ctrls.set(V4L2_CID_VBLANK, static_cast<int32_t>(vblank));
 
 	setSensorControls.emit(frame, ctrls);
 }
diff --git a/src/ipa/rppx1/stats.h b/src/ipa/rppx1/stats.h
index 39dc132bac..2925e2bbfc 100644
--- a/src/ipa/rppx1/stats.h
+++ b/src/ipa/rppx1/stats.h
@@ -16,6 +16,8 @@  namespace libcamera {
 namespace ipa::rppx1 {
 
 enum class StatsType : uint16_t {
+	ExmPre1,
+	HistPost,
 };
 
 namespace details {
@@ -32,6 +34,9 @@  struct stats_type {
 			RPPX1_STATS_BLOCK_TYPE_##id;			\
 	};
 
+RPPX1_DEFINE_STATS_TYPE(ExmPre1, exm, EXM_PRE1)
+RPPX1_DEFINE_STATS_TYPE(HistPost, hist, HIST_POST)
+
 struct stats_traits {
 	using id_type = StatsType;