summaryrefslogtreecommitdiff
path: root/src/ipa/simple
diff options
context:
space:
mode:
Diffstat (limited to 'src/ipa/simple')
-rw-r--r--src/ipa/simple/algorithms/agc.cpp139
-rw-r--r--src/ipa/simple/algorithms/agc.h33
-rw-r--r--src/ipa/simple/algorithms/algorithm.h22
-rw-r--r--src/ipa/simple/algorithms/awb.cpp69
-rw-r--r--src/ipa/simple/algorithms/awb.h32
-rw-r--r--src/ipa/simple/algorithms/blc.cpp95
-rw-r--r--src/ipa/simple/algorithms/blc.h36
-rw-r--r--src/ipa/simple/algorithms/lut.cpp86
-rw-r--r--src/ipa/simple/algorithms/lut.h34
-rw-r--r--src/ipa/simple/algorithms/meson.build8
-rw-r--r--src/ipa/simple/black_level.cpp88
-rw-r--r--src/ipa/simple/black_level.h29
-rw-r--r--src/ipa/simple/data/uncalibrated.yaml5
-rw-r--r--src/ipa/simple/ipa_context.cpp102
-rw-r--r--src/ipa/simple/ipa_context.h69
-rw-r--r--src/ipa/simple/meson.build12
-rw-r--r--src/ipa/simple/module.h30
-rw-r--r--src/ipa/simple/soft_simple.cpp300
18 files changed, 868 insertions, 321 deletions
diff --git a/src/ipa/simple/algorithms/agc.cpp b/src/ipa/simple/algorithms/agc.cpp
new file mode 100644
index 00000000..df92edd7
--- /dev/null
+++ b/src/ipa/simple/algorithms/agc.cpp
@@ -0,0 +1,139 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat Inc.
+ *
+ * Exposure and gain
+ */
+
+#include "agc.h"
+
+#include <stdint.h>
+
+#include <libcamera/base/log.h>
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(IPASoftExposure)
+
+namespace ipa::soft::algorithms {
+
+/*
+ * The number of bins to use for the optimal exposure calculations.
+ */
+static constexpr unsigned int kExposureBinsCount = 5;
+
+/*
+ * The exposure is optimal when the mean sample value of the histogram is
+ * in the middle of the range.
+ */
+static constexpr float kExposureOptimal = kExposureBinsCount / 2.0;
+
+/*
+ * This implements the hysteresis for the exposure adjustment.
+ * It is small enough to have the exposure close to the optimal, and is big
+ * enough to prevent the exposure from wobbling around the optimal value.
+ */
+static constexpr float kExposureSatisfactory = 0.2;
+
+Agc::Agc()
+{
+}
+
+void Agc::updateExposure(IPAContext &context, double exposureMSV)
+{
+ /*
+ * kExpDenominator of 10 gives ~10% increment/decrement;
+ * kExpDenominator of 5 - about ~20%
+ */
+ static constexpr uint8_t kExpDenominator = 10;
+ static constexpr uint8_t kExpNumeratorUp = kExpDenominator + 1;
+ static constexpr uint8_t kExpNumeratorDown = kExpDenominator - 1;
+
+ double next;
+ int32_t &exposure = context.activeState.agc.exposure;
+ double &again = context.activeState.agc.again;
+
+ if (exposureMSV < kExposureOptimal - kExposureSatisfactory) {
+ next = exposure * kExpNumeratorUp / kExpDenominator;
+ if (next - exposure < 1)
+ exposure += 1;
+ else
+ exposure = next;
+ if (exposure >= context.configuration.agc.exposureMax) {
+ next = again * kExpNumeratorUp / kExpDenominator;
+ if (next - again < context.configuration.agc.againMinStep)
+ again += context.configuration.agc.againMinStep;
+ else
+ again = next;
+ }
+ }
+
+ if (exposureMSV > kExposureOptimal + kExposureSatisfactory) {
+ if (exposure == context.configuration.agc.exposureMax &&
+ again > context.configuration.agc.againMin) {
+ next = again * kExpNumeratorDown / kExpDenominator;
+ if (again - next < context.configuration.agc.againMinStep)
+ again -= context.configuration.agc.againMinStep;
+ else
+ again = next;
+ } else {
+ next = exposure * kExpNumeratorDown / kExpDenominator;
+ if (exposure - next < 1)
+ exposure -= 1;
+ else
+ exposure = next;
+ }
+ }
+
+ exposure = std::clamp(exposure, context.configuration.agc.exposureMin,
+ context.configuration.agc.exposureMax);
+ again = std::clamp(again, context.configuration.agc.againMin,
+ context.configuration.agc.againMax);
+
+ LOG(IPASoftExposure, Debug)
+ << "exposureMSV " << exposureMSV
+ << " exp " << exposure << " again " << again;
+}
+
+void Agc::process(IPAContext &context,
+ [[maybe_unused]] const uint32_t frame,
+ [[maybe_unused]] IPAFrameContext &frameContext,
+ const SwIspStats *stats,
+ [[maybe_unused]] ControlList &metadata)
+{
+ /*
+ * Calculate Mean Sample Value (MSV) according to formula from:
+ * https://www.araa.asn.au/acra/acra2007/papers/paper84final.pdf
+ */
+ const auto &histogram = stats->yHistogram;
+ const unsigned int blackLevelHistIdx =
+ context.activeState.blc.level / (256 / SwIspStats::kYHistogramSize);
+ const unsigned int histogramSize =
+ SwIspStats::kYHistogramSize - blackLevelHistIdx;
+ const unsigned int yHistValsPerBin = histogramSize / kExposureBinsCount;
+ const unsigned int yHistValsPerBinMod =
+ histogramSize / (histogramSize % kExposureBinsCount + 1);
+ int exposureBins[kExposureBinsCount] = {};
+ unsigned int denom = 0;
+ unsigned int num = 0;
+
+ for (unsigned int i = 0; i < histogramSize; i++) {
+ unsigned int idx = (i - (i / yHistValsPerBinMod)) / yHistValsPerBin;
+ exposureBins[idx] += histogram[blackLevelHistIdx + i];
+ }
+
+ for (unsigned int i = 0; i < kExposureBinsCount; i++) {
+ LOG(IPASoftExposure, Debug) << i << ": " << exposureBins[i];
+ denom += exposureBins[i];
+ num += exposureBins[i] * (i + 1);
+ }
+
+ float exposureMSV = (denom == 0 ? 0 : static_cast<float>(num) / denom);
+ updateExposure(context, exposureMSV);
+}
+
+REGISTER_IPA_ALGORITHM(Agc, "Agc")
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/agc.h b/src/ipa/simple/algorithms/agc.h
new file mode 100644
index 00000000..ad5fca9f
--- /dev/null
+++ b/src/ipa/simple/algorithms/agc.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat Inc.
+ *
+ * Exposure and gain
+ */
+
+#pragma once
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::soft::algorithms {
+
+class Agc : public Algorithm
+{
+public:
+ Agc();
+ ~Agc() = default;
+
+ void process(IPAContext &context, const uint32_t frame,
+ IPAFrameContext &frameContext,
+ const SwIspStats *stats,
+ ControlList &metadata) override;
+
+private:
+ void updateExposure(IPAContext &context, double exposureMSV);
+};
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/algorithm.h b/src/ipa/simple/algorithms/algorithm.h
new file mode 100644
index 00000000..41f63170
--- /dev/null
+++ b/src/ipa/simple/algorithms/algorithm.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024 Red Hat, Inc.
+ *
+ * Software ISP control algorithm interface
+ */
+
+#pragma once
+
+#include <libipa/algorithm.h>
+
+#include "module.h"
+
+namespace libcamera {
+
+namespace ipa::soft {
+
+using Algorithm = libcamera::ipa::Algorithm<Module>;
+
+} /* namespace ipa::soft */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/awb.cpp b/src/ipa/simple/algorithms/awb.cpp
new file mode 100644
index 00000000..195de41d
--- /dev/null
+++ b/src/ipa/simple/algorithms/awb.cpp
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat Inc.
+ *
+ * Auto white balance
+ */
+
+#include "awb.h"
+
+#include <numeric>
+#include <stdint.h>
+
+#include <libcamera/base/log.h>
+
+#include "simple/ipa_context.h"
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(IPASoftAwb)
+
+namespace ipa::soft::algorithms {
+
+int Awb::configure(IPAContext &context,
+ [[maybe_unused]] const IPAConfigInfo &configInfo)
+{
+ auto &gains = context.activeState.gains;
+ gains.red = gains.green = gains.blue = 1.0;
+
+ return 0;
+}
+
+void Awb::process(IPAContext &context,
+ [[maybe_unused]] const uint32_t frame,
+ [[maybe_unused]] IPAFrameContext &frameContext,
+ const SwIspStats *stats,
+ [[maybe_unused]] ControlList &metadata)
+{
+ const SwIspStats::Histogram &histogram = stats->yHistogram;
+ const uint8_t blackLevel = context.activeState.blc.level;
+
+ /*
+ * Black level must be subtracted to get the correct AWB ratios, they
+ * would be off if they were computed from the whole brightness range
+ * rather than from the sensor range.
+ */
+ const uint64_t nPixels = std::accumulate(
+ histogram.begin(), histogram.end(), 0);
+ const uint64_t offset = blackLevel * nPixels;
+ const uint64_t sumR = stats->sumR_ - offset / 4;
+ const uint64_t sumG = stats->sumG_ - offset / 2;
+ const uint64_t sumB = stats->sumB_ - offset / 4;
+
+ /*
+ * Calculate red and blue gains for AWB.
+ * Clamp max gain at 4.0, this also avoids 0 division.
+ */
+ auto &gains = context.activeState.gains;
+ gains.red = sumR <= sumG / 4 ? 4.0 : static_cast<double>(sumG) / sumR;
+ gains.blue = sumB <= sumG / 4 ? 4.0 : static_cast<double>(sumG) / sumB;
+ /* Green gain is fixed to 1.0 */
+
+ LOG(IPASoftAwb, Debug) << "gain R/B " << gains.red << "/" << gains.blue;
+}
+
+REGISTER_IPA_ALGORITHM(Awb, "Awb")
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/awb.h b/src/ipa/simple/algorithms/awb.h
new file mode 100644
index 00000000..db1496cd
--- /dev/null
+++ b/src/ipa/simple/algorithms/awb.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat Inc.
+ *
+ * Auto white balance
+ */
+
+#pragma once
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::soft::algorithms {
+
+class Awb : public Algorithm
+{
+public:
+ Awb() = default;
+ ~Awb() = default;
+
+ int configure(IPAContext &context, const IPAConfigInfo &configInfo) override;
+ void process(IPAContext &context,
+ const uint32_t frame,
+ IPAFrameContext &frameContext,
+ const SwIspStats *stats,
+ ControlList &metadata) override;
+};
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/blc.cpp b/src/ipa/simple/algorithms/blc.cpp
new file mode 100644
index 00000000..b4e32fe1
--- /dev/null
+++ b/src/ipa/simple/algorithms/blc.cpp
@@ -0,0 +1,95 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat Inc.
+ *
+ * Black level handling
+ */
+
+#include "blc.h"
+
+#include <numeric>
+
+#include <libcamera/base/log.h>
+
+namespace libcamera {
+
+namespace ipa::soft::algorithms {
+
+LOG_DEFINE_CATEGORY(IPASoftBL)
+
+BlackLevel::BlackLevel()
+{
+}
+
+int BlackLevel::init(IPAContext &context, const YamlObject &tuningData)
+{
+ auto blackLevel = tuningData["blackLevel"].get<int16_t>();
+ if (blackLevel.has_value()) {
+ /*
+ * Convert 16 bit values from the tuning file to 8 bit black
+ * level for the SoftISP.
+ */
+ context.configuration.black.level = blackLevel.value() >> 8;
+ }
+ return 0;
+}
+
+int BlackLevel::configure(IPAContext &context,
+ [[maybe_unused]] const IPAConfigInfo &configInfo)
+{
+ context.activeState.blc.level =
+ context.configuration.black.level.value_or(255);
+ return 0;
+}
+
+void BlackLevel::process(IPAContext &context,
+ [[maybe_unused]] const uint32_t frame,
+ IPAFrameContext &frameContext,
+ const SwIspStats *stats,
+ [[maybe_unused]] ControlList &metadata)
+{
+ if (context.configuration.black.level.has_value())
+ return;
+
+ if (frameContext.sensor.exposure == exposure_ &&
+ frameContext.sensor.gain == gain_) {
+ return;
+ }
+
+ const SwIspStats::Histogram &histogram = stats->yHistogram;
+
+ /*
+ * The constant is selected to be "good enough", not overly
+ * conservative or aggressive. There is no magic about the given value.
+ */
+ constexpr float ignoredPercentage = 0.02;
+ const unsigned int total =
+ std::accumulate(begin(histogram), end(histogram), 0);
+ const unsigned int pixelThreshold = ignoredPercentage * total;
+ const unsigned int histogramRatio = 256 / SwIspStats::kYHistogramSize;
+ const unsigned int currentBlackIdx =
+ context.activeState.blc.level / histogramRatio;
+
+ for (unsigned int i = 0, seen = 0;
+ i < currentBlackIdx && i < SwIspStats::kYHistogramSize;
+ i++) {
+ seen += histogram[i];
+ if (seen >= pixelThreshold) {
+ context.activeState.blc.level = i * histogramRatio;
+ exposure_ = frameContext.sensor.exposure;
+ gain_ = frameContext.sensor.gain;
+ LOG(IPASoftBL, Debug)
+ << "Auto-set black level: "
+ << i << "/" << SwIspStats::kYHistogramSize
+ << " (" << 100 * (seen - histogram[i]) / total << "% below, "
+ << 100 * seen / total << "% at or below)";
+ break;
+ }
+ };
+}
+
+REGISTER_IPA_ALGORITHM(BlackLevel, "BlackLevel")
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/blc.h b/src/ipa/simple/algorithms/blc.h
new file mode 100644
index 00000000..2cf2a877
--- /dev/null
+++ b/src/ipa/simple/algorithms/blc.h
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat Inc.
+ *
+ * Black level handling
+ */
+
+#pragma once
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::soft::algorithms {
+
+class BlackLevel : public Algorithm
+{
+public:
+ BlackLevel();
+ ~BlackLevel() = default;
+
+ int init(IPAContext &context, const YamlObject &tuningData) override;
+ int configure(IPAContext &context, const IPAConfigInfo &configInfo) override;
+ void process(IPAContext &context, const uint32_t frame,
+ IPAFrameContext &frameContext,
+ const SwIspStats *stats,
+ ControlList &metadata) override;
+
+private:
+ uint32_t exposure_;
+ double gain_;
+};
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/lut.cpp b/src/ipa/simple/algorithms/lut.cpp
new file mode 100644
index 00000000..9744e773
--- /dev/null
+++ b/src/ipa/simple/algorithms/lut.cpp
@@ -0,0 +1,86 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat Inc.
+ *
+ * Color lookup tables construction
+ */
+
+#include "lut.h"
+
+#include <algorithm>
+#include <cmath>
+#include <stdint.h>
+
+#include <libcamera/base/log.h>
+
+#include "simple/ipa_context.h"
+
+namespace libcamera {
+
+namespace ipa::soft::algorithms {
+
+int Lut::configure(IPAContext &context,
+ [[maybe_unused]] const IPAConfigInfo &configInfo)
+{
+ /* Gamma value is fixed */
+ context.configuration.gamma = 0.5;
+ updateGammaTable(context);
+
+ return 0;
+}
+
+void Lut::updateGammaTable(IPAContext &context)
+{
+ auto &gammaTable = context.activeState.gamma.gammaTable;
+ auto blackLevel = context.activeState.blc.level;
+ const unsigned int blackIndex = blackLevel * gammaTable.size() / 256;
+
+ std::fill(gammaTable.begin(), gammaTable.begin() + blackIndex, 0);
+ const float divisor = gammaTable.size() - blackIndex - 1.0;
+ for (unsigned int i = blackIndex; i < gammaTable.size(); i++)
+ gammaTable[i] = UINT8_MAX * std::pow((i - blackIndex) / divisor,
+ context.configuration.gamma);
+
+ context.activeState.gamma.blackLevel = blackLevel;
+}
+
+void Lut::prepare(IPAContext &context,
+ [[maybe_unused]] const uint32_t frame,
+ [[maybe_unused]] IPAFrameContext &frameContext,
+ [[maybe_unused]] DebayerParams *params)
+{
+ /*
+ * Update the gamma table if needed. This means if black level changes
+ * and since the black level gets updated only if a lower value is
+ * observed, it's not permanently prone to minor fluctuations or
+ * rounding errors.
+ */
+ if (context.activeState.gamma.blackLevel != context.activeState.blc.level)
+ updateGammaTable(context);
+
+ auto &gains = context.activeState.gains;
+ auto &gammaTable = context.activeState.gamma.gammaTable;
+ const unsigned int gammaTableSize = gammaTable.size();
+
+ for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
+ const double div = static_cast<double>(DebayerParams::kRGBLookupSize) /
+ gammaTableSize;
+ /* Apply gamma after gain! */
+ unsigned int idx;
+ idx = std::min({ static_cast<unsigned int>(i * gains.red / div),
+ gammaTableSize - 1 });
+ params->red[i] = gammaTable[idx];
+ idx = std::min({ static_cast<unsigned int>(i * gains.green / div),
+ gammaTableSize - 1 });
+ params->green[i] = gammaTable[idx];
+ idx = std::min({ static_cast<unsigned int>(i * gains.blue / div),
+ gammaTableSize - 1 });
+ params->blue[i] = gammaTable[idx];
+ }
+}
+
+REGISTER_IPA_ALGORITHM(Lut, "Lut")
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/lut.h b/src/ipa/simple/algorithms/lut.h
new file mode 100644
index 00000000..b635987d
--- /dev/null
+++ b/src/ipa/simple/algorithms/lut.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat Inc.
+ *
+ * Color lookup tables construction
+ */
+
+#pragma once
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::soft::algorithms {
+
+class Lut : public Algorithm
+{
+public:
+ Lut() = default;
+ ~Lut() = default;
+
+ int configure(IPAContext &context, const IPAConfigInfo &configInfo) override;
+ void prepare(IPAContext &context,
+ const uint32_t frame,
+ IPAFrameContext &frameContext,
+ DebayerParams *params) override;
+
+private:
+ void updateGammaTable(IPAContext &context);
+};
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/meson.build b/src/ipa/simple/algorithms/meson.build
new file mode 100644
index 00000000..37a2eb53
--- /dev/null
+++ b/src/ipa/simple/algorithms/meson.build
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: CC0-1.0
+
+soft_simple_ipa_algorithms = files([
+ 'awb.cpp',
+ 'agc.cpp',
+ 'blc.cpp',
+ 'lut.cpp',
+])
diff --git a/src/ipa/simple/black_level.cpp b/src/ipa/simple/black_level.cpp
deleted file mode 100644
index cc490eb5..00000000
--- a/src/ipa/simple/black_level.cpp
+++ /dev/null
@@ -1,88 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-/*
- * Copyright (C) 2024, Red Hat Inc.
- *
- * black level handling
- */
-
-#include "black_level.h"
-
-#include <numeric>
-
-#include <libcamera/base/log.h>
-
-namespace libcamera {
-
-LOG_DEFINE_CATEGORY(IPASoftBL)
-
-/**
- * \class BlackLevel
- * \brief Object providing black point level for software ISP
- *
- * Black level can be provided in hardware tuning files or, if no tuning file is
- * available for the given hardware, guessed automatically, with less accuracy.
- * As tuning files are not yet implemented for software ISP, BlackLevel
- * currently provides only guessed black levels.
- *
- * This class serves for tracking black level as a property of the underlying
- * hardware, not as means of enhancing a particular scene or image.
- *
- * The class is supposed to be instantiated for the given camera stream.
- * The black level can be retrieved using BlackLevel::get() method. It is
- * initially 0 and may change when updated using BlackLevel::update() method.
- */
-
-BlackLevel::BlackLevel()
- : blackLevel_(255), blackLevelSet_(false)
-{
-}
-
-/**
- * \brief Return the current black level
- *
- * \return The black level, in the range from 0 (minimum) to 255 (maximum).
- * If the black level couldn't be determined yet, return 0.
- */
-uint8_t BlackLevel::get() const
-{
- return blackLevelSet_ ? blackLevel_ : 0;
-}
-
-/**
- * \brief Update black level from the provided histogram
- * \param[in] yHistogram The histogram to be used for updating black level
- *
- * The black level is property of the given hardware, not image. It is updated
- * only if it has not been yet set or if it is lower than the lowest value seen
- * so far.
- */
-void BlackLevel::update(SwIspStats::Histogram &yHistogram)
-{
- /*
- * The constant is selected to be "good enough", not overly conservative or
- * aggressive. There is no magic about the given value.
- */
- constexpr float ignoredPercentage_ = 0.02;
- const unsigned int total =
- std::accumulate(begin(yHistogram), end(yHistogram), 0);
- const unsigned int pixelThreshold = ignoredPercentage_ * total;
- const unsigned int histogramRatio = 256 / SwIspStats::kYHistogramSize;
- const unsigned int currentBlackIdx = blackLevel_ / histogramRatio;
-
- for (unsigned int i = 0, seen = 0;
- i < currentBlackIdx && i < SwIspStats::kYHistogramSize;
- i++) {
- seen += yHistogram[i];
- if (seen >= pixelThreshold) {
- blackLevel_ = i * histogramRatio;
- blackLevelSet_ = true;
- LOG(IPASoftBL, Debug)
- << "Auto-set black level: "
- << i << "/" << SwIspStats::kYHistogramSize
- << " (" << 100 * (seen - yHistogram[i]) / total << "% below, "
- << 100 * seen / total << "% at or below)";
- break;
- }
- };
-}
-} /* namespace libcamera */
diff --git a/src/ipa/simple/black_level.h b/src/ipa/simple/black_level.h
deleted file mode 100644
index 5e032f9f..00000000
--- a/src/ipa/simple/black_level.h
+++ /dev/null
@@ -1,29 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-/*
- * Copyright (C) 2024, Red Hat Inc.
- *
- * black level handling
- */
-
-#pragma once
-
-#include <array>
-#include <stdint.h>
-
-#include "libcamera/internal/software_isp/swisp_stats.h"
-
-namespace libcamera {
-
-class BlackLevel
-{
-public:
- BlackLevel();
- uint8_t get() const;
- void update(SwIspStats::Histogram &yHistogram);
-
-private:
- uint8_t blackLevel_;
- bool blackLevelSet_;
-};
-
-} /* namespace libcamera */
diff --git a/src/ipa/simple/data/uncalibrated.yaml b/src/ipa/simple/data/uncalibrated.yaml
index ff981a1a..3f147112 100644
--- a/src/ipa/simple/data/uncalibrated.yaml
+++ b/src/ipa/simple/data/uncalibrated.yaml
@@ -2,4 +2,9 @@
%YAML 1.1
---
version: 1
+algorithms:
+ - BlackLevel:
+ - Awb:
+ - Lut:
+ - Agc:
...
diff --git a/src/ipa/simple/ipa_context.cpp b/src/ipa/simple/ipa_context.cpp
new file mode 100644
index 00000000..3f94bbeb
--- /dev/null
+++ b/src/ipa/simple/ipa_context.cpp
@@ -0,0 +1,102 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2021, Google Inc.
+ * Copyright (C) 2024 Red Hat Inc.
+ *
+ * Software ISP IPA Context
+ */
+
+#include "ipa_context.h"
+
+/**
+ * \file ipa_context.h
+ * \brief Context and state information shared between the algorithms
+ */
+
+namespace libcamera::ipa::soft {
+
+/**
+ * \struct IPASessionConfiguration
+ * \brief Session configuration for the IPA module
+ *
+ * The session configuration contains all IPA configuration parameters that
+ * remain constant during the capture session, from IPA module start to stop.
+ * It is typically set during the configure() operation of the IPA module, but
+ * may also be updated in the start() operation.
+ */
+
+/**
+ * \struct IPAActiveState
+ * \brief The active state of the IPA algorithms
+ *
+ * The IPA is fed with the statistics generated from the latest frame processed.
+ * The statistics are then processed by the IPA algorithms to compute parameters
+ * required for the next frame capture and processing. The current state of the
+ * algorithms is reflected through the IPAActiveState to store the values most
+ * recently computed by the IPA algorithms.
+ */
+
+/**
+ * \struct IPAContext
+ * \brief Global IPA context data shared between all algorithms
+ *
+ * \var IPAContext::configuration
+ * \brief The IPA session configuration, immutable during the session
+ *
+ * \var IPAContext::frameContexts
+ * \brief Ring buffer of the IPAFrameContext(s)
+ *
+ * \var IPAContext::activeState
+ * \brief The current state of IPA algorithms
+ */
+
+/**
+ * \var IPASessionConfiguration::gamma
+ * \brief Gamma value to be used in the raw image processing
+ */
+
+/**
+ * \var IPAActiveState::black
+ * \brief Context for the Black Level algorithm
+ *
+ * \var IPAActiveState::black.level
+ * \brief Current determined black level
+ */
+
+/**
+ * \var IPAActiveState::gains
+ * \brief Context for gains in the Colors algorithm
+ *
+ * \var IPAActiveState::gains.red
+ * \brief Gain of red color
+ *
+ * \var IPAActiveState::gains.green
+ * \brief Gain of green color
+ *
+ * \var IPAActiveState::gains.blue
+ * \brief Gain of blue color
+ */
+
+/**
+ * \var IPAActiveState::agc
+ * \brief Context for the AGC algorithm
+ *
+ * \var IPAActiveState::agc.exposure
+ * \brief Current exposure value
+ *
+ * \var IPAActiveState::agc.again
+ * \brief Current analog gain value
+ */
+
+/**
+ * \var IPAActiveState::gamma
+ * \brief Context for gamma in the Colors algorithm
+ *
+ * \var IPAActiveState::gamma.gammaTable
+ * \brief Computed gamma table
+ *
+ * \var IPAActiveState::gamma.blackLevel
+ * \brief Black level used for the gamma table computation
+ */
+
+} /* namespace libcamera::ipa::soft */
diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
new file mode 100644
index 00000000..fd121eeb
--- /dev/null
+++ b/src/ipa/simple/ipa_context.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024 Red Hat, Inc.
+ *
+ * Simple pipeline IPA Context
+ */
+
+#pragma once
+
+#include <array>
+#include <optional>
+#include <stdint.h>
+
+#include <libipa/fc_queue.h>
+
+namespace libcamera {
+
+namespace ipa::soft {
+
+struct IPASessionConfiguration {
+ float gamma;
+ struct {
+ int32_t exposureMin, exposureMax;
+ double againMin, againMax, againMinStep;
+ } agc;
+ struct {
+ std::optional<uint8_t> level;
+ } black;
+};
+
+struct IPAActiveState {
+ struct {
+ uint8_t level;
+ } blc;
+
+ struct {
+ double red;
+ double green;
+ double blue;
+ } gains;
+
+ struct {
+ int32_t exposure;
+ double again;
+ } agc;
+
+ static constexpr unsigned int kGammaLookupSize = 1024;
+ struct {
+ std::array<double, kGammaLookupSize> gammaTable;
+ uint8_t blackLevel;
+ } gamma;
+};
+
+struct IPAFrameContext : public FrameContext {
+ struct {
+ uint32_t exposure;
+ double gain;
+ } sensor;
+};
+
+struct IPAContext {
+ IPASessionConfiguration configuration;
+ IPAActiveState activeState;
+ FCQueue<IPAFrameContext> frameContexts;
+};
+
+} /* namespace ipa::soft */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/meson.build b/src/ipa/simple/meson.build
index 33d1c96a..2f9f15f4 100644
--- a/src/ipa/simple/meson.build
+++ b/src/ipa/simple/meson.build
@@ -1,14 +1,18 @@
# SPDX-License-Identifier: CC0-1.0
+subdir('algorithms')
+subdir('data')
+
ipa_name = 'ipa_soft_simple'
soft_simple_sources = files([
+ 'ipa_context.cpp',
'soft_simple.cpp',
- 'black_level.cpp',
])
-mod = shared_module(ipa_name,
- [soft_simple_sources, libcamera_generated_ipa_headers],
+soft_simple_sources += soft_simple_ipa_algorithms
+
+mod = shared_module(ipa_name, soft_simple_sources,
name_prefix : '',
include_directories : [ipa_includes],
dependencies : [libcamera_private, libipa_dep],
@@ -24,6 +28,4 @@ if ipa_sign_module
build_by_default : true)
endif
-subdir('data')
-
ipa_names += ipa_name
diff --git a/src/ipa/simple/module.h b/src/ipa/simple/module.h
new file mode 100644
index 00000000..8d4d53fb
--- /dev/null
+++ b/src/ipa/simple/module.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024 Red Hat, Inc.
+ *
+ * Software ISP IPA Module
+ */
+
+#pragma once
+
+#include <libcamera/controls.h>
+
+#include <libcamera/ipa/soft_ipa_interface.h>
+
+#include "libcamera/internal/software_isp/debayer_params.h"
+#include "libcamera/internal/software_isp/swisp_stats.h"
+
+#include <libipa/module.h>
+
+#include "ipa_context.h"
+
+namespace libcamera {
+
+namespace ipa::soft {
+
+using Module = ipa::Module<IPAContext, IPAFrameContext, IPAConfigInfo,
+ DebayerParams, SwIspStats>;
+
+} /* namespace ipa::soft */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/soft_simple.cpp b/src/ipa/simple/soft_simple.cpp
index b7746ce0..ac2a9421 100644
--- a/src/ipa/simple/soft_simple.cpp
+++ b/src/ipa/simple/soft_simple.cpp
@@ -5,8 +5,6 @@
* Simple Software Image Processing Algorithm module
*/
-#include <cmath>
-#include <numeric>
#include <stdint.h>
#include <sys/mman.h>
@@ -29,37 +27,21 @@
#include "libipa/camera_sensor_helper.h"
-#include "black_level.h"
+#include "module.h"
namespace libcamera {
LOG_DEFINE_CATEGORY(IPASoft)
namespace ipa::soft {
-/*
- * The number of bins to use for the optimal exposure calculations.
- */
-static constexpr unsigned int kExposureBinsCount = 5;
-
-/*
- * The exposure is optimal when the mean sample value of the histogram is
- * in the middle of the range.
- */
-static constexpr float kExposureOptimal = kExposureBinsCount / 2.0;
-
-/*
- * The below value implements the hysteresis for the exposure adjustment.
- * It is small enough to have the exposure close to the optimal, and is big
- * enough to prevent the exposure from wobbling around the optimal value.
- */
-static constexpr float kExposureSatisfactory = 0.2;
+/* Maximum number of frame contexts to be held */
+static constexpr uint32_t kMaxFrameContexts = 16;
-class IPASoftSimple : public ipa::soft::IPASoftInterface
+class IPASoftSimple : public ipa::soft::IPASoftInterface, public Module
{
public:
IPASoftSimple()
- : params_(nullptr), stats_(nullptr), blackLevel_(BlackLevel()),
- ignoreUpdates_(0)
+ : context_({ {}, {}, { kMaxFrameContexts } })
{
}
@@ -69,12 +51,18 @@ public:
const SharedFD &fdStats,
const SharedFD &fdParams,
const ControlInfoMap &sensorInfoMap) override;
- int configure(const ControlInfoMap &sensorInfoMap) override;
+ int configure(const IPAConfigInfo &configInfo) override;
int start() override;
void stop() override;
- void processStats(const ControlList &sensorControls) override;
+ void queueRequest(const uint32_t frame, const ControlList &controls) override;
+ void computeParams(const uint32_t frame) override;
+ void processStats(const uint32_t frame, const uint32_t bufferId,
+ const ControlList &sensorControls) override;
+
+protected:
+ std::string logPrefix() const override;
private:
void updateExposure(double exposureMSV);
@@ -83,17 +71,9 @@ private:
SwIspStats *stats_;
std::unique_ptr<CameraSensorHelper> camHelper_;
ControlInfoMap sensorInfoMap_;
- BlackLevel blackLevel_;
- static constexpr unsigned int kGammaLookupSize = 1024;
- std::array<uint8_t, kGammaLookupSize> gammaTable_;
- int lastBlackLevel_ = -1;
-
- int32_t exposureMin_, exposureMax_;
- int32_t exposure_;
- double againMin_, againMax_, againMinStep_;
- double again_;
- unsigned int ignoreUpdates_;
+ /* Local parameter storage */
+ struct IPAContext context_;
};
IPASoftSimple::~IPASoftSimple()
@@ -134,6 +114,15 @@ int IPASoftSimple::init(const IPASettings &settings,
unsigned int version = (*data)["version"].get<uint32_t>(0);
LOG(IPASoft, Debug) << "Tuning file version " << version;
+ if (!data->contains("algorithms")) {
+ LOG(IPASoft, Error) << "Tuning file doesn't contain algorithms";
+ return -EINVAL;
+ }
+
+ int ret = createAlgorithms(context_, (*data)["algorithms"]);
+ if (ret)
+ return ret;
+
params_ = nullptr;
stats_ = nullptr;
@@ -188,27 +177,46 @@ int IPASoftSimple::init(const IPASettings &settings,
return 0;
}
-int IPASoftSimple::configure(const ControlInfoMap &sensorInfoMap)
+int IPASoftSimple::configure(const IPAConfigInfo &configInfo)
{
- sensorInfoMap_ = sensorInfoMap;
+ sensorInfoMap_ = configInfo.sensorControls;
const ControlInfo &exposureInfo = sensorInfoMap_.find(V4L2_CID_EXPOSURE)->second;
const ControlInfo &gainInfo = sensorInfoMap_.find(V4L2_CID_ANALOGUE_GAIN)->second;
- exposureMin_ = exposureInfo.min().get<int32_t>();
- exposureMax_ = exposureInfo.max().get<int32_t>();
- if (!exposureMin_) {
+ /* Clear the IPA context before the streaming session. */
+ context_.configuration = {};
+ context_.activeState = {};
+ context_.frameContexts.clear();
+
+ context_.configuration.agc.exposureMin = exposureInfo.min().get<int32_t>();
+ context_.configuration.agc.exposureMax = exposureInfo.max().get<int32_t>();
+ if (!context_.configuration.agc.exposureMin) {
LOG(IPASoft, Warning) << "Minimum exposure is zero, that can't be linear";
- exposureMin_ = 1;
+ context_.configuration.agc.exposureMin = 1;
}
int32_t againMin = gainInfo.min().get<int32_t>();
int32_t againMax = gainInfo.max().get<int32_t>();
if (camHelper_) {
- againMin_ = camHelper_->gain(againMin);
- againMax_ = camHelper_->gain(againMax);
- againMinStep_ = (againMax_ - againMin_) / 100.0;
+ context_.configuration.agc.againMin = camHelper_->gain(againMin);
+ context_.configuration.agc.againMax = camHelper_->gain(againMax);
+ context_.configuration.agc.againMinStep =
+ (context_.configuration.agc.againMax -
+ context_.configuration.agc.againMin) /
+ 100.0;
+ if (!context_.configuration.black.level.has_value() &&
+ camHelper_->blackLevel().has_value()) {
+ /*
+ * The black level from camHelper_ is a 16 bit value, software ISP
+ * works with 8 bit pixel values, both regardless of the actual
+ * sensor pixel width. Hence we obtain the pixel-based black value
+ * by dividing the value from the helper by 256.
+ */
+ context_.configuration.black.level =
+ camHelper_->blackLevel().value() / 256;
+ }
} else {
/*
* The camera sensor gain (g) is usually not equal to the value written
@@ -220,18 +228,28 @@ int IPASoftSimple::configure(const ControlInfoMap &sensorInfoMap)
* the AGC algorithm (abrupt near one edge, and very small near the
* other) we limit the range of the gain values used.
*/
- againMax_ = againMax;
+ context_.configuration.agc.againMax = againMax;
if (!againMin) {
LOG(IPASoft, Warning)
<< "Minimum gain is zero, that can't be linear";
- againMin_ = std::min(100, againMin / 2 + againMax / 2);
+ context_.configuration.agc.againMin =
+ std::min(100, againMin / 2 + againMax / 2);
}
- againMinStep_ = 1.0;
+ context_.configuration.agc.againMinStep = 1.0;
}
- LOG(IPASoft, Info) << "Exposure " << exposureMin_ << "-" << exposureMax_
- << ", gain " << againMin_ << "-" << againMax_
- << " (" << againMinStep_ << ")";
+ for (auto const &algo : algorithms()) {
+ int ret = algo->configure(context_, configInfo);
+ if (ret)
+ return ret;
+ }
+
+ LOG(IPASoft, Info)
+ << "Exposure " << context_.configuration.agc.exposureMin << "-"
+ << context_.configuration.agc.exposureMax
+ << ", gain " << context_.configuration.agc.againMin << "-"
+ << context_.configuration.agc.againMax
+ << " (" << context_.configuration.agc.againMinStep << ")";
return 0;
}
@@ -243,107 +261,45 @@ int IPASoftSimple::start()
void IPASoftSimple::stop()
{
+ context_.frameContexts.clear();
}
-void IPASoftSimple::processStats(const ControlList &sensorControls)
+void IPASoftSimple::queueRequest(const uint32_t frame, const ControlList &controls)
{
- SwIspStats::Histogram histogram = stats_->yHistogram;
- if (ignoreUpdates_ > 0)
- blackLevel_.update(histogram);
- const uint8_t blackLevel = blackLevel_.get();
-
- /*
- * Black level must be subtracted to get the correct AWB ratios, they
- * would be off if they were computed from the whole brightness range
- * rather than from the sensor range.
- */
- const uint64_t nPixels = std::accumulate(
- histogram.begin(), histogram.end(), 0);
- const uint64_t offset = blackLevel * nPixels;
- const uint64_t sumR = stats_->sumR_ - offset / 4;
- const uint64_t sumG = stats_->sumG_ - offset / 2;
- const uint64_t sumB = stats_->sumB_ - offset / 4;
-
- /*
- * Calculate red and blue gains for AWB.
- * Clamp max gain at 4.0, this also avoids 0 division.
- * Gain: 128 = 0.5, 256 = 1.0, 512 = 2.0, etc.
- */
- const unsigned int gainR = sumR <= sumG / 4 ? 1024 : 256 * sumG / sumR;
- const unsigned int gainB = sumB <= sumG / 4 ? 1024 : 256 * sumG / sumB;
- /* Green gain and gamma values are fixed */
- constexpr unsigned int gainG = 256;
-
- /* Update the gamma table if needed */
- if (blackLevel != lastBlackLevel_) {
- constexpr float gamma = 0.5;
- const unsigned int blackIndex = blackLevel * kGammaLookupSize / 256;
- std::fill(gammaTable_.begin(), gammaTable_.begin() + blackIndex, 0);
- const float divisor = kGammaLookupSize - blackIndex - 1.0;
- for (unsigned int i = blackIndex; i < kGammaLookupSize; i++)
- gammaTable_[i] = UINT8_MAX *
- std::pow((i - blackIndex) / divisor, gamma);
-
- lastBlackLevel_ = blackLevel;
- }
-
- for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
- constexpr unsigned int div =
- DebayerParams::kRGBLookupSize * 256 / kGammaLookupSize;
- unsigned int idx;
-
- /* Apply gamma after gain! */
- idx = std::min({ i * gainR / div, (kGammaLookupSize - 1) });
- params_->red[i] = gammaTable_[idx];
+ IPAFrameContext &frameContext = context_.frameContexts.alloc(frame);
- idx = std::min({ i * gainG / div, (kGammaLookupSize - 1) });
- params_->green[i] = gammaTable_[idx];
-
- idx = std::min({ i * gainB / div, (kGammaLookupSize - 1) });
- params_->blue[i] = gammaTable_[idx];
- }
+ for (auto const &algo : algorithms())
+ algo->queueRequest(context_, frame, frameContext, controls);
+}
+void IPASoftSimple::computeParams(const uint32_t frame)
+{
+ IPAFrameContext &frameContext = context_.frameContexts.get(frame);
+ for (auto const &algo : algorithms())
+ algo->prepare(context_, frame, frameContext, params_);
setIspParams.emit();
+}
- /* \todo Switch to the libipa/algorithm.h API someday. */
+void IPASoftSimple::processStats(const uint32_t frame,
+ [[maybe_unused]] const uint32_t bufferId,
+ const ControlList &sensorControls)
+{
+ IPAFrameContext &frameContext = context_.frameContexts.get(frame);
- /*
- * AE / AGC, use 2 frames delay to make sure that the exposure and
- * the gain set have applied to the camera sensor.
- * \todo This could be handled better with DelayedControls.
- */
- if (ignoreUpdates_ > 0) {
- --ignoreUpdates_;
- return;
- }
+ frameContext.sensor.exposure =
+ sensorControls.get(V4L2_CID_EXPOSURE).get<int32_t>();
+ int32_t again = sensorControls.get(V4L2_CID_ANALOGUE_GAIN).get<int32_t>();
+ frameContext.sensor.gain = camHelper_ ? camHelper_->gain(again) : again;
/*
- * Calculate Mean Sample Value (MSV) according to formula from:
- * https://www.araa.asn.au/acra/acra2007/papers/paper84final.pdf
+ * Software ISP currently does not produce any metadata. Use an empty
+ * ControlList for now.
+ *
+ * \todo Implement proper metadata handling
*/
- const unsigned int blackLevelHistIdx =
- blackLevel / (256 / SwIspStats::kYHistogramSize);
- const unsigned int histogramSize =
- SwIspStats::kYHistogramSize - blackLevelHistIdx;
- const unsigned int yHistValsPerBin = histogramSize / kExposureBinsCount;
- const unsigned int yHistValsPerBinMod =
- histogramSize / (histogramSize % kExposureBinsCount + 1);
- int exposureBins[kExposureBinsCount] = {};
- unsigned int denom = 0;
- unsigned int num = 0;
-
- for (unsigned int i = 0; i < histogramSize; i++) {
- unsigned int idx = (i - (i / yHistValsPerBinMod)) / yHistValsPerBin;
- exposureBins[idx] += stats_->yHistogram[blackLevelHistIdx + i];
- }
-
- for (unsigned int i = 0; i < kExposureBinsCount; i++) {
- LOG(IPASoft, Debug) << i << ": " << exposureBins[i];
- denom += exposureBins[i];
- num += exposureBins[i] * (i + 1);
- }
-
- float exposureMSV = static_cast<float>(num) / denom;
+ ControlList metadata(controls::controls);
+ for (auto const &algo : algorithms())
+ algo->process(context_, frame, frameContext, stats_, metadata);
/* Sanity check */
if (!sensorControls.contains(V4L2_CID_EXPOSURE) ||
@@ -352,73 +308,19 @@ void IPASoftSimple::processStats(const ControlList &sensorControls)
return;
}
- exposure_ = sensorControls.get(V4L2_CID_EXPOSURE).get<int32_t>();
- int32_t again = sensorControls.get(V4L2_CID_ANALOGUE_GAIN).get<int32_t>();
- again_ = camHelper_ ? camHelper_->gain(again) : again;
-
- updateExposure(exposureMSV);
-
ControlList ctrls(sensorInfoMap_);
- ctrls.set(V4L2_CID_EXPOSURE, exposure_);
+ auto &againNew = context_.activeState.agc.again;
+ ctrls.set(V4L2_CID_EXPOSURE, context_.activeState.agc.exposure);
ctrls.set(V4L2_CID_ANALOGUE_GAIN,
- static_cast<int32_t>(camHelper_ ? camHelper_->gainCode(again_) : again_));
-
- ignoreUpdates_ = 2;
+ static_cast<int32_t>(camHelper_ ? camHelper_->gainCode(againNew) : againNew));
setSensorControls.emit(ctrls);
-
- LOG(IPASoft, Debug) << "exposureMSV " << exposureMSV
- << " exp " << exposure_ << " again " << again_
- << " gain R/B " << gainR << "/" << gainB
- << " black level " << static_cast<unsigned int>(blackLevel);
}
-void IPASoftSimple::updateExposure(double exposureMSV)
+std::string IPASoftSimple::logPrefix() const
{
- /*
- * kExpDenominator of 10 gives ~10% increment/decrement;
- * kExpDenominator of 5 - about ~20%
- */
- static constexpr uint8_t kExpDenominator = 10;
- static constexpr uint8_t kExpNumeratorUp = kExpDenominator + 1;
- static constexpr uint8_t kExpNumeratorDown = kExpDenominator - 1;
-
- double next;
-
- if (exposureMSV < kExposureOptimal - kExposureSatisfactory) {
- next = exposure_ * kExpNumeratorUp / kExpDenominator;
- if (next - exposure_ < 1)
- exposure_ += 1;
- else
- exposure_ = next;
- if (exposure_ >= exposureMax_) {
- next = again_ * kExpNumeratorUp / kExpDenominator;
- if (next - again_ < againMinStep_)
- again_ += againMinStep_;
- else
- again_ = next;
- }
- }
-
- if (exposureMSV > kExposureOptimal + kExposureSatisfactory) {
- if (exposure_ == exposureMax_ && again_ > againMin_) {
- next = again_ * kExpNumeratorDown / kExpDenominator;
- if (again_ - next < againMinStep_)
- again_ -= againMinStep_;
- else
- again_ = next;
- } else {
- next = exposure_ * kExpNumeratorDown / kExpDenominator;
- if (exposure_ - next < 1)
- exposure_ -= 1;
- else
- exposure_ = next;
- }
- }
-
- exposure_ = std::clamp(exposure_, exposureMin_, exposureMax_);
- again_ = std::clamp(again_, againMin_, againMax_);
+ return "IPASoft";
}
} /* namespace ipa::soft */