From patchwork Fri Sep 18 12:09:42 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28352 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 28525C3360 for ; Fri, 18 Sep 2026 12:10:20 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id A3A7968756; Fri, 18 Sep 2026 14:10:14 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="sy9PDH6M"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id DBBD268743 for ; Fri, 18 Sep 2026 14:09:56 +0200 (CEST) Received: from pb-laptop.local (185.221.142.0.nat.pool.zt.hu [185.221.142.0]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 057C34C2F; Fri, 18 Sep 2026 14:08:13 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1789733294; bh=aYKsCycrDJz8kGry49Vm1LP4uGF4BUdnzkwpwR3VKCw=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=sy9PDH6M6lXJUJx5d25JAi+svGLBCImS4coNo755k1H2SylssXAZhVDB9Jha2lHa4 3bXE2t1CAd422hVYBtvsMySR5i5Cz9pGuG0KtqVKJ5aJXoryfyqGlHF8RzaOLcsC71 8KSy86PUtydvd4r14hLxyNQQK4NSfXVp2q3eer8o= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi Subject: [PATCH v3 14/21] ipa: rppx1: agc: Add Date: Fri, 18 Sep 2026 14:09:42 +0200 Message-ID: <20260918120949.191668-15-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260918120949.191668-1-barnabas.pocze@ideasonboard.com> References: <20260918120949.191668-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" From: Jacopo Mondi Add the algorithm to the rppx1 ipa module based on the corresponding algorithm in the rkisp1 ipa module. Signed-off-by: Jacopo Mondi --- 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 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 +#include +#include +#include + +#include +#include + +#include +#include + +#include + +#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>().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 meteringModes; + std::vector 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(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(*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(); + 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(); + 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 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(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 expMeans, std::span 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(expMeans_[i] * gain, kMaxExpMean) * w; + wSum += w; + } + + /* \todo Weight with the AWB gains */ + + return ySum / wSum / kMaxExpMean; + } + +private: + std::span expMeans_; + std::span 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(); + const auto exmPre1 = stats->block(); + + 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 +#include + +#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> 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 -#include +#include #include #include -#include #include +#include #include #include @@ -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 + #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(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(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;