new file mode 100644
@@ -0,0 +1,263 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas On Board
+ *
+ * RkISP2 Lens Shading Correction control
+ */
+
+#include "lsc.h"
+
+#include <algorithm>
+#include <cmath>
+#include <numeric>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
+
+/**
+ * \file lsc.h
+ */
+
+namespace libcamera {
+
+namespace ipa::rkisp2::algorithms {
+
+/**
+ * \class LensShadingCorrection
+ * \brief RkISP2 Lens Shading Correction control
+ */
+
+LOG_DEFINE_CATEGORY(RkISP2Lsc)
+
+namespace {
+
+constexpr int kColourTemperatureQuantization = 10;
+
+unsigned int quantize(unsigned int value, unsigned int step)
+{
+ return std::lround(value / static_cast<double>(step)) * step;
+}
+
+} /* namespace */
+
+LensShadingCorrection::LensShadingCorrection()
+ : lastAppliedCt_(0), lastAppliedQuantizedCt_(0)
+{
+}
+
+std::vector<double> LensShadingCorrection::parseSizes(const ValueNode &tuningData,
+ const char *prop)
+{
+ std::vector<double> sizes =
+ tuningData[prop].get<std::vector<double>>().value_or(utils::defopt);
+ /* Nobody cares about 8x8 mirrored mode; we'll just use 16x16 mode */
+ if (sizes.size() != RKISP2_ISP_LSC_SECTORS_TBL_SIZE_MAX) {
+ LOG(RkISP2Lsc, Error)
+ << "Invalid '" << prop << "' values: expected "
+ << RKISP2_ISP_LSC_SECTORS_TBL_SIZE_MAX
+ << " elements, got " << sizes.size();
+ return {};
+ }
+
+ /*
+ * The sum of all elements must be 1 to satisfy hardware constraints.
+ * Validate it here, allowing a 1% tolerance as rounding errors may
+ * prevent an exact match (further adjustments will be performed in
+ * LensShadingCorrection::prepare()).
+ *
+ * If we were in 8x8 mode then we'd have to mirror the quadrants like
+ * in rkisp1, but in 16x16 mode we get to configure the entire table.
+ * Since 8x8 table support is a todo, we only need to handle the 16x16
+ * case here thus the sum should be 1.
+ *
+ * \todo Support 8x8 mode?
+ */
+ double sum = std::accumulate(sizes.begin(), sizes.end(), 0.0);
+ if (sum < 0.95 || sum > 1.05) {
+ LOG(RkISP2Lsc, Error)
+ << "Invalid '" << prop << "' values: sum of the elements"
+ << " should be 1.0, got " << sum;
+ return {};
+ }
+
+ return sizes;
+}
+
+std::vector<double> LensShadingCorrection::sizesToPositions(Span<const double> sizes)
+{
+ std::vector<double> positions(sizes.size() + 1);
+
+ positions[0] = 0.0;
+ for (size_t i = 1; i < positions.size(); i++)
+ positions[i] = positions[i - 1] + sizes[i - 1];
+
+ return positions;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::init
+ */
+int LensShadingCorrection::init(IPAContext &context,
+ const ValueNode &tuningData)
+{
+ xSize_ = parseSizes(tuningData, "x-size");
+ ySize_ = parseSizes(tuningData, "y-size");
+
+ if (xSize_.empty() || ySize_.empty())
+ return -EINVAL;
+
+ xPos_ = sizesToPositions(xSize_);
+ yPos_ = sizesToPositions(ySize_);
+
+ return lscAlgo_.init(tuningData, context.ctrlMap, {
+ .keys = { "r", "gr", "gb", "b" },
+ .numHCells = RKISP2_ISP_LSC_SAMPLES_MAX,
+ .numVCells = RKISP2_ISP_LSC_SAMPLES_MAX,
+ .sensorSize = context.sensorInfo.activeAreaSize
+ });
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::configure
+ */
+int LensShadingCorrection::configure(IPAContext &context,
+ const IPACameraSensorInfo &configInfo)
+{
+ const Size &size = context.configuration.sensor.size;
+ Size totalSize{};
+
+ /* Calculate gradients. */
+ for (unsigned int i = 0; i < RKISP2_ISP_LSC_SECTORS_TBL_SIZE_MAX; ++i) {
+ xSizes_[i] = xSize_[i] * size.width;
+ ySizes_[i] = ySize_[i] * size.height;
+
+ /*
+ * To prevent unexpected behavior of the ISP, the sum of
+ * x_sizes and y_sizes items shall be equal to
+ * respectively size.width and size.height. Enforce it by
+ * computing the last tables value to avoid
+ * rounding-induced errors.
+ */
+ if (i == RKISP2_ISP_LSC_SECTORS_TBL_SIZE_MAX - 1) {
+ xSizes_[i] = size.width - totalSize.width;
+ ySizes_[i] = size.height - totalSize.height;
+ }
+
+ totalSize.width += xSizes_[i];
+ totalSize.height += ySizes_[i];
+
+ xGrad_[i] = std::round(32768 / xSizes_[i]);
+ yGrad_[i] = std::round(32768 / ySizes_[i]);
+ }
+
+ return lscAlgo_.configure(context.activeState.lsc, configInfo.analogCrop,
+ xPos_, yPos_);
+}
+
+void LensShadingCorrection::setParameters(rkisp2_params_lsc &config)
+{
+ memcpy(config.x_grads, xGrad_, sizeof(config.x_grads));
+ memcpy(config.y_grads, yGrad_, sizeof(config.y_grads));
+ memcpy(config.x_sizes, xSizes_, sizeof(config.x_sizes));
+ memcpy(config.y_sizes, ySizes_, sizeof(config.y_sizes));
+}
+
+void LensShadingCorrection::copyTable(rkisp2_params_lsc &config,
+ const ipa::lsc::Components<uint16_t> &set)
+{
+ const auto &r = set.at("r");
+ std::copy(r.begin(), r.end(), &config.r_data_tbl[0][0][0]);
+ const auto &gr = set.at("gr");
+ std::copy(gr.begin(), gr.end(), &config.gr_data_tbl[0][0][0]);
+ const auto &gb = set.at("gb");
+ std::copy(gb.begin(), gb.end(), &config.gb_data_tbl[0][0][0]);
+ const auto &b = set.at("b");
+ std::copy(b.begin(), b.end(), &config.b_data_tbl[0][0][0]);
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::queueRequest
+ */
+void LensShadingCorrection::queueRequest(IPAContext &context,
+ [[maybe_unused]] const uint32_t frame,
+ IPAFrameContext &frameContext,
+ const ControlList &controls)
+{
+ lscAlgo_.queueRequest(context.activeState.lsc, frameContext.lsc,
+ controls);
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::prepare
+ */
+void LensShadingCorrection::prepare([[maybe_unused]] IPAContext &context,
+ [[maybe_unused]] const uint32_t frame,
+ IPAFrameContext &frameContext,
+ RkISP2Params *params)
+{
+ uint32_t ct = frameContext.awb.temperatureK;
+ unsigned int quantizedCt = quantize(ct, kColourTemperatureQuantization);
+
+ /* Check if we can skip the update. */
+ if (!frameContext.lsc.update) {
+ if (!frameContext.lsc.enabled)
+ return;
+
+ /*
+ * Add a threshold so that oscillations around a quantization
+ * step don't lead to constant changes.
+ */
+ if (utils::abs_diff(ct, lastAppliedCt_) < kColourTemperatureQuantization / 2)
+ return;
+
+ if (quantizedCt == lastAppliedQuantizedCt_)
+ return;
+ }
+
+ auto config = params->block<RkISP2Blocks::Lsc>();
+ config.setEnabled(frameContext.lsc.enabled);
+
+ if (!frameContext.lsc.enabled)
+ return;
+
+ /*
+ * \todo Should add support for the lsc table swapping functionality?
+ * Or maybe we don't need it because the lsc doesn't change very
+ * frequently. Just use the 0th table for now.
+ */
+ config->window_mode = RKISP2_ISP_LSC_CONFIG_16X16;
+ config->write_table[0] = 1;
+ config->write_table[1] = 0;
+ config->active_table = 0;
+ config->set_active_table_when = RKISP2_ISP_LSC_SET_ACTIVE_TABLE_AFTER;
+
+ setParameters(*config);
+
+ const auto &set = lscAlgo_.interpolateComponents(quantizedCt);
+ copyTable(*config, set);
+
+ lastAppliedCt_ = ct;
+ lastAppliedQuantizedCt_ = quantizedCt;
+
+ LOG(RkISP2Lsc, Debug)
+ << "ct is " << ct << ", quantized to "
+ << quantizedCt;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::process
+ */
+void LensShadingCorrection::process([[maybe_unused]] IPAContext &context,
+ [[maybe_unused]] const uint32_t frame,
+ IPAFrameContext &frameContext,
+ [[maybe_unused]] const rkisp2_stats_buffer *stats,
+ ControlList &metadata)
+{
+ lscAlgo_.process(frameContext.lsc, metadata);
+}
+
+REGISTER_IPA_ALGORITHM(LensShadingCorrection, "LensShadingCorrection")
+
+} /* namespace ipa::rkisp2::algorithms */
+
+} /* namespace libcamera */
new file mode 100644
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas On Board
+ *
+ * RkISP2 Lens Shading Correction algorithm
+ */
+
+#pragma once
+
+#include <vector>
+
+#include <linux/rkisp2-config.h>
+
+#include "libcamera/internal/value_node.h"
+
+#include "libipa/fixedpoint.h"
+#include "libipa/lsc.h"
+
+#include "algorithm.h"
+#include "ipa_context.h"
+#include "params.h"
+
+namespace libcamera {
+
+namespace ipa::rkisp2::algorithms {
+
+class LensShadingCorrection : public Algorithm
+{
+public:
+ LensShadingCorrection();
+ ~LensShadingCorrection() = default;
+
+ 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,
+ RkISP2Params *params) override;
+ void process(IPAContext &context, const uint32_t frame,
+ IPAFrameContext &frameContext,
+ const rkisp2_stats_buffer *stats,
+ ControlList &metadata) override;
+
+private:
+ std::vector<double> parseSizes(const ValueNode &tuningData,
+ const char *prop);
+ std::vector<double> sizesToPositions(Span<const double> sizes);
+
+ void setParameters(rkisp2_params_lsc &config);
+ void copyTable(rkisp2_params_lsc &config,
+ const ipa::lsc::Components<uint16_t> &set0);
+
+ std::vector<double> xSize_;
+ std::vector<double> ySize_;
+ uint16_t xGrad_[RKISP2_ISP_LSC_SECTORS_TBL_SIZE_MAX];
+ uint16_t yGrad_[RKISP2_ISP_LSC_SECTORS_TBL_SIZE_MAX];
+ uint16_t xSizes_[RKISP2_ISP_LSC_SECTORS_TBL_SIZE_MAX];
+ uint16_t ySizes_[RKISP2_ISP_LSC_SECTORS_TBL_SIZE_MAX];
+ std::vector<double> xPos_;
+ std::vector<double> yPos_;
+
+ unsigned int lastAppliedCt_;
+ unsigned int lastAppliedQuantizedCt_;
+
+ LscAlgorithm<uint16_t, UQ<3, 10>> lscAlgo_;
+};
+
+} /* namespace ipa::rkisp2::algorithms */
+} /* namespace libcamera */
@@ -7,5 +7,6 @@ rkisp2_ipa_algorithms = files([
'ccm.cpp',
'csm.cpp',
'goc.cpp',
+ 'lsc.cpp',
])
Implement a lens shading correction algorithm for the rkisp2 IPA. It uses the libipa lens shading correction. Signed-off-by: Paul Elder <paul.elder@ideasonboard.com> --- src/ipa/rkisp2/algorithms/lsc.cpp | 263 ++++++++++++++++++++++++++ src/ipa/rkisp2/algorithms/lsc.h | 71 +++++++ src/ipa/rkisp2/algorithms/meson.build | 1 + 3 files changed, 335 insertions(+) create mode 100644 src/ipa/rkisp2/algorithms/lsc.cpp create mode 100644 src/ipa/rkisp2/algorithms/lsc.h