@@ -66,12 +66,37 @@ static constexpr float kExpProportionalGain = 0.04;
*/
static constexpr float kExpMaxStep = 0.15;
+/*
+ * Errors above this threshold are far from the target, and the small
+ * proportional steps would need tens of statistics periods to get there
+ * (a statistics period being several frames, and the frame possibly long
+ * in low light). For those, jump by the ratio between the target and the
+ * measured MSV instead, bounded by kExpMaxJump per step. The proportional
+ * correction takes over near the target, so the convergence stays smooth.
+ */
+static constexpr float kExpLargeError = 0.5;
+static constexpr float kExpMaxJump = 2.0;
+
+/*
+ * Digital gain is applied by the ISP on top of the sensor exposure and
+ * analogue gain, only when those are exhausted. It doesn't add information,
+ * so the tuning file bounds it with maxDigitalGain (1.0 disables it).
+ */
+static constexpr double kDefaultMaxDigitalGain = 1.0;
+
Agc::Agc()
{
}
-int Agc::init(IPAContext &context, [[maybe_unused]] const ValueNode &tuningData)
+int Agc::init(IPAContext &context, const ValueNode &tuningData)
{
+ maxDigitalGain_ = tuningData["maxDigitalGain"].get<double>(kDefaultMaxDigitalGain);
+ if (maxDigitalGain_ < 1.0) {
+ LOG(IPASoftIspExposure, Warning)
+ << "maxDigitalGain " << maxDigitalGain_ << " below 1.0, ignored";
+ maxDigitalGain_ = 1.0;
+ }
+
/*
* Expose the frame duration limits the sensor can achieve in the
* current mode. Whether the IPA can actually change the frame duration
@@ -116,10 +141,24 @@ int Agc::configure(IPAContext &context, [[maybe_unused]] const IPAConfigInfo &co
agc.maxFrameDuration = agc.minFrameDuration;
}
agc.vblank = cfg.vblankDef;
+ agc.dgain = 1.0;
+ context.configuration.agc.dgainMax = maxDigitalGain_;
return 0;
}
+void Agc::prepare(IPAContext &context, [[maybe_unused]] const uint32_t frame,
+ IPAFrameContext &frameContext, DebayerParams *params)
+{
+ /*
+ * The digital gain scales the colour gains the ISP applies after black
+ * level subtraction. This runs after the AWB has set the gains, as the
+ * Agc algorithm is listed after Awb in the tuning file.
+ */
+ frameContext.agc.digitalGain = context.activeState.agc.dgain;
+ params->gains *= frameContext.agc.digitalGain;
+}
+
void Agc::queueRequest(IPAContext &context, [[maybe_unused]] const uint32_t frame,
IPAFrameContext &frameContext, const ControlList &controls)
{
@@ -201,6 +240,8 @@ void Agc::updateExposure(IPAContext &context, IPAFrameContext &frameContext, dou
int32_t &exposure = frameContext.sensor.exposure;
double &again = frameContext.sensor.gain;
int32_t &vblank = frameContext.sensor.vblank;
+ double &dgain = frameContext.agc.digitalGain;
+ const double dgainMax = context.configuration.agc.dgainMax;
int32_t vblankLo, vblankHi;
vblankRange(context, frameContext, vblankLo, vblankHi);
@@ -227,32 +268,47 @@ void Agc::updateExposure(IPAContext &context, IPAFrameContext &frameContext, dou
}
/*
- * Compute a proportional correction factor. The sign of the error
- * determines the direction: positive error means too dark (increase),
- * negative means too bright (decrease).
+ * Compute the correction factor. The sign of the error determines the
+ * direction: positive error means too dark (increase), negative means
+ * too bright (decrease). Far from the target, jump by the measured
+ * ratio; near it, apply a small proportional step.
*/
- float step = std::clamp(static_cast<float>(error) * kExpProportionalGain,
- -kExpMaxStep, kExpMaxStep);
- float factor = 1.0f + step;
+ float factor;
+ if (std::abs(error) > kExpLargeError) {
+ factor = std::clamp(static_cast<float>(kExposureOptimal / std::max(exposureMSV, 0.1)),
+ 1.0f / kExpMaxJump, kExpMaxJump);
+ } else {
+ float step = std::clamp(static_cast<float>(error) * kExpProportionalGain,
+ -kExpMaxStep, kExpMaxStep);
+ factor = 1.0f + step;
+ }
if (factor > 1.0f) {
/*
* Scene too dark: increase exposure first (lengthening the
- * frame when the limits allow it), then gain.
+ * frame when the limits allow it), then analogue gain, then
+ * digital gain.
*/
if (exposure < exposureMax) {
int32_t next = static_cast<int32_t>(exposure * factor);
exposure = std::max(next, exposure + 1);
- } else {
+ } else if (again < context.configuration.agc.againMax) {
double next = again * factor;
if (next - again < context.configuration.agc.againMinStep)
again += context.configuration.agc.againMinStep;
else
again = next;
+ } else {
+ dgain = std::min(dgain * factor, dgainMax);
}
} else {
- /* Scene too bright: decrease gain first, then exposure. */
- if (again > context.configuration.agc.again10) {
+ /*
+ * Scene too bright: decrease digital gain first, then analogue
+ * gain, then exposure.
+ */
+ if (dgain > 1.0) {
+ dgain = std::max(dgain * factor, 1.0);
+ } else if (again > context.configuration.agc.again10) {
double next = again * factor;
if (again - next < context.configuration.agc.againMinStep)
again -= context.configuration.agc.againMinStep;
@@ -272,14 +328,18 @@ void Agc::updateExposure(IPAContext &context, IPAFrameContext &frameContext, dou
static_cast<int32_t>(context.configuration.agc.frameHeight),
vblankLo, vblankHi);
+ dgain = std::clamp(dgain, 1.0, dgainMax);
+
context.activeState.agc.exposure = exposure;
context.activeState.agc.again = again;
context.activeState.agc.vblank = vblank;
+ context.activeState.agc.dgain = dgain;
LOG(IPASoftIspExposure, Debug)
<< "exposureMSV " << exposureMSV
<< " error " << error << " factor " << factor
<< " exp " << exposure << " again " << again
+ << " dgain " << dgain
<< " vblank " << vblank << " (" << vblankLo << "-" << vblankHi << ")";
}
@@ -293,6 +353,7 @@ void Agc::process(IPAContext &context,
utils::Duration exposureTime = cfg.lineDuration * frameContext.sensor.exposure;
metadata.set(controls::ExposureTime, exposureTime.get<std::micro>());
metadata.set(controls::AnalogueGain, frameContext.sensor.gain);
+ metadata.set(controls::DigitalGain, static_cast<float>(frameContext.agc.digitalGain));
if (cfg.vblankSupported) {
frameContext.agc.frameDuration =
cfg.lineDuration * (cfg.frameHeight + frameContext.sensor.vblank);
@@ -319,6 +380,7 @@ void Agc::process(IPAContext &context,
frameContext.sensor.exposure = context.activeState.agc.exposure;
frameContext.sensor.gain = context.activeState.agc.again;
frameContext.sensor.vblank = context.activeState.agc.vblank;
+ frameContext.agc.digitalGain = context.activeState.agc.dgain;
return;
}
@@ -344,8 +406,16 @@ void Agc::process(IPAContext &context,
return;
}
+ /*
+ * The statistics are computed on the sensor data, before the ISP
+ * applies the digital gain. Scale the histogram index by the digital
+ * gain of the frame so that the MSV reflects the output brightness.
+ */
+ const double digitalGain = frameContext.agc.digitalGain;
for (unsigned int i = 0; i < histogramSize; i++) {
- unsigned int idx = (i - (i / yHistValsPerBinMod)) / yHistValsPerBin;
+ unsigned int scaled = std::min<unsigned int>(
+ static_cast<unsigned int>(i * digitalGain), histogramSize - 1);
+ unsigned int idx = (scaled - (scaled / yHistValsPerBinMod)) / yHistValsPerBin;
exposureBins[idx] += histogram[blackLevelHistIdx + i];
}
@@ -24,6 +24,9 @@ public:
void queueRequest(IPAContext &context, const uint32_t frame,
IPAFrameContext &frameContext,
const ControlList &controls) override;
+ void prepare(IPAContext &context, const uint32_t frame,
+ IPAFrameContext &frameContext,
+ DebayerParams *params) override;
void process(IPAContext &context, const uint32_t frame,
IPAFrameContext &frameContext,
const SwIspStats *stats,
@@ -34,6 +37,8 @@ private:
void vblankRange(const IPAContext &context, const IPAFrameContext &frameContext,
int32_t &vblankLo, int32_t &vblankHi) const;
int32_t exposureMaxForVblank(const IPAContext &context, int32_t vblank) const;
+
+ double maxDigitalGain_;
};
} /* namespace ipa::softisp::algorithms */
@@ -42,6 +42,8 @@ struct IPASessionConfiguration {
/* Lines the sensor keeps between max exposure and frame length */
int32_t exposureMargin;
uint32_t frameHeight;
+ /* Digital gain applied by the ISP after the sensor gain is exhausted */
+ double dgainMax;
} agc;
struct {
std::optional<uint8_t> level;
@@ -56,6 +58,7 @@ struct IPAActiveState {
int32_t exposure;
double again;
int32_t vblank;
+ double dgain;
bool valid;
utils::Duration minFrameDuration;
utils::Duration maxFrameDuration;
@@ -91,6 +94,7 @@ struct IPAFrameContext : public FrameContext {
utils::Duration minFrameDuration;
utils::Duration maxFrameDuration;
utils::Duration frameDuration;
+ double digitalGain;
} agc;
float gamma;
The AGC corrects the exposure by a small proportional step per statistics period, at most 15% and typically around 5%. Getting from a black start to a bright target then needs tens of statistics periods, and as each period spans several frames and the frames get long in low light, the convergence takes tens of seconds. Far from the target, jump by the ratio between the target and the measured mean sample value, bounded to a factor of two per step; the proportional correction takes over near the target so the convergence stays smooth. When the exposure and the analogue gain are both exhausted, apply a digital gain in the ISP by scaling the colour gains, after black level subtraction. It doesn't add information, so it is only used last and is bounded by the new maxDigitalGain tuning parameter (default 1.0, which disables it). The statistics are computed on the sensor data, so the histogram is scaled by the digital gain of the frame before the mean sample value is computed. The gain is reported in the DigitalGain metadata. Signed-off-by: Robert Bozik <robertbozik@gmail.com> --- src/ipa/softisp/algorithms/agc.cpp | 94 ++++++++++++++++++++++++++---- src/ipa/softisp/algorithms/agc.h | 5 ++ src/ipa/softisp/ipa_context.h | 4 ++ 3 files changed, 91 insertions(+), 12 deletions(-)