======= Sample AE implementation in C++ =======
The code below provides a sample implementation of the AE algorithm described in [[Automatic exposure]]. It relies on a few external dependencies outlined below:
* ''%%S2R::IImgSensor%%'' from the [[code:SUB2r-lib]] - a conveniency library to access image sensor's chip and control its registers
* ''%%S2R::ImgStats%%'' - image's statistical data, mainly histograms. This structure is not very well documented in here but should be fairly obvious from its usage
* ''%%S2R::enum HIST_TYPE%%'' - a helper enum used as an index into histogram data arrays
The code below conforms to C++17 standard and it is recommended to use the C++17 toolchain as this is the version the [[code::SUB2r-lib]] is targetting.
====== Helpers ======
A few short functions for code readability, put into a local anonymous namespace (effectively making these function ''static'' and making sure we don't pollute/collide with the global namespace).
namespace{
constexpr int defectivePixels = 100; // assume there are at least that many DP that will screw our stats
int _triggeredLeft(const ImgStats& _stats, PerPart _pp){
auto count = max(defectivePixels, _stats.ae.pixels * _pp);
for(int i = 0; i < 256; ++i){
count -= _stats.ae.hist[i];
if(count < 0){
return i;
}
}
return 255;
}
int _triggeredRight(const ImgStats& _stats, PerPart _pp){
auto count = max(defectivePixels, _stats.ae.pixels * _pp);
for(int i = 255; i >= 0; --i){
count -= _stats.ae.hist[i];
if(count < 0){
return i;
}
}
return 0;
}
}
====== PerPart ======
This helper class is to avoid stupid arithmetic errors when mixing various ratios, like absolute ratios, percentages (%), per-milles(‰), parts-per-million (ppm), etc. It also helps with gracefully handling division by zero (which results in defaulting the result of such ratio to ''0'').
In addition a few helpful user-defined literals are provided for better code readability when it comes to constants.
This is a super-minimalistic class that doesn't pretend to be production-quality.
namespace{
struct PerPart{
PerPart(long double _val = 0.f) : m_val(_val) {} // 1.0f == 100_pcnt
PerPart(unsigned long long int _val, unsigned long long int _div)
: PerPart(_div == 0 ? 0.f : static_cast(_val) / _div)
{}
operator long double() const { return m_val; }
int pcnt() const { return static_cast(round(m_val * 100)); }
private:
long double m_val = 0.f;
};
PerPart operator"" _pcnt(unsigned long long int _val){return PerPart(_val, 100);}
PerPart operator"" _pmille(unsigned long long int _val){return PerPart(_val, 1000);}
PerPart operator"" _ppm(unsigned long long int _val){return PerPart(_val, 1000 * 1000);}
}
====== AE ======
Main auto-exposure implementation class.
The unnamed internal ''struct m_vars'' is here to provide an overview of the notable results of calculation and a convenient way to display debug info in the UI part of the program (which is more or less irrelevant for the on-device implementation and therefore is kept to the minimum in this sample implementation).
Primarily the class is used in 3 steps:
- Instantiate an object
- Call its functor
- (Optionally) display the results in UI
===== Declaration =====
struct AE{
enum class Action{noop, inc, dec}; // suggested action to take
enum class CorrOp{noop, inc_gl, inc_exp, inc_green
, dec_gl, dec_exp, dec_green}; // performed correction
AE(IImgSensor * _ov, const ImgStats & _stats, const PerPart & _pp, int _aeTgtLuma);
// analyze and, if asked and is necessary, adjust sensor's config
auto operator()(bool _performCorrection)
{
_analyzeLH();
_analyzeRH();
if(_performCorrection){
if(m_vars.actionLH != Action::noop) _correctLH();
if(m_vars.actionRH != Action::noop) _correctRH();
}
return m_vars;
}
const auto & details() const {return m_vars;} // diagnostics to display
private:
void _analyzeLH();
void _analyzeRH();
void _correctLH();
void _correctRH();
void _aeInc();
void _aeDec();
private:
using Val = IImgSensor::Value; // shortcut for typing out the full type qualifier
IImgSensor & m_ov; // OmniVision sensor interface
const PerPart m_hysteresis; // tolerance +/- for how accurate/sensitive the correction should be
struct{
int underExposed = 0; // left-most bars on the histogram it takes to cover the N tolerance pixels
int overExposed = 255; // right-most bars on the histogram it takes to cover the N tolerance pixels
int adjBL = 0;
int avgY = 0; // actual arithmetic average of Y in [0..255]
double adjY = 1.; // requested brightness adjustment for the image
Action actionLH = Action::noop;
Action actionRH = Action::noop;
CorrOp op = CorrOp::noop;
} m_vars;
};
===== Constructor AE::AE =====
A bulk of (quite simplistic) calculations is performed with data in ''_stats'' and the results are stored in the ''m_vars'' struct:
AE::AE(IImgSensor * _ov, const ImgStats & _stats, const PerPart & _pp, int _tgtLuma, const PerPart & _hysteresis)
: m_ov(*_ov)
, m_hysteresis(_hysteresis)
{
m_vars.underExposed = _triggeredLeft(_stats, _pp);
m_vars.overExposed = _triggeredRight(_stats, _pp);
m_vars.avgY = static_cast(_stats.ae.pixels == 0 ? 0 : _stats.ae.y / _stats.ae.pixels);
m_vars.adjY = m_vars.avgY == 0 ? 0 : 1. * _tgtLuma / m_vars.avgY;
}
===== AE::_analyzeLH() =====
Decide what to do with the "left side of the histogram", which is controlled by the ''Black Level'' value:
void AE::_analyzeLH()
{
// try to reduce the BL if the image is severely overexposed
{
const auto diffLeft = m_vars.underExposed;
const auto diffRight = 256 - m_vars.overExposed;
if(diffLeft > diffRight * 2){
const int currBL = m_ov[Val::black_level];
if(currBL > 0){
m_vars.adjBL = - max(1, currBL / 10);
m_vars.actionLH = Action::dec;
}else{
m_vars.adjBL = 0;
m_vars.actionLH = Action::noop;
}
return;
}
}
if(m_vars.underExposed > 2 && m_vars.underExposed < 20){
m_vars.actionLH = Action::noop; // no-op if we are in that sweet spot
}else{
// strive to be get into sweet spot
const int defaultBL = 16;
m_vars.adjBL = (defaultBL - m_vars.underExposed) * 5 / 8;
if(m_vars.adjBL > 1){
m_vars.actionLH = Action::inc;
}else if(m_vars.adjBL < 1){
m_vars.actionLH = Action::dec;
}else{
m_vars.actionLH = Action::noop;
}
}
}
===== AE::_analyzeRH() =====
Decide what to do with the "right side of the histogram", which greatly affects the overall brightness of the image and the amount of overexposure produced by the image sensor:
void AE::_analyzeRH()
{
m_vars.adjY += (1 - m_vars.adjY) * 5 / 8; // be 62.5% less aggressive than requested to avoid oscillations
if(m_vars.overExposed > 60 && m_vars.adjY < 1 - m_hysteresis){
m_vars.actionRH = Action::dec; // plenty of pixels at level 60+ and we were asked to dim the image
}else if(m_vars.overExposed > 250 && m_vars.adjY <= 1 - m_hysteresis){
m_vars.actionRH = Action::dec; // too many overexposed pixels and NOT increasing brightness
}else if(m_vars.overExposed < 250 && m_vars.adjY > 1 + m_hysteresis){
m_vars.actionRH = Action::inc; // there's room to grow and asked to increase brightness
}else{
m_vars.actionRH = Action::noop;
}
}
===== AE::_correctLH() =====
Once the Black Level correction is determined the code to perform that action is quite trivial:
void AE::_correctLH()
{
auto bl = m_ov[Val::black_level];
bl = std::clamp(bl + m_vars.adjBL, 0, m_ov.getLimit(Val::black_level));
}
===== AE::_correctRH() =====
"Right hand side" correction is a bit more involved and is thus split into to distinct functions, one for increasing the brightness and another for decreasing it:
void AE::_correctRH()
{
switch(m_vars.actionRH){
case Action::inc: _aeInc(); break;
case Action::dec: _aeDec(); break;
}
}
===== AE::_aeInc() =====
Increasing the brightness of the image is a 3-step process where first we max out the ''Exposure'' (as it produces the least amount of side-effects), then we are increasing the ''Global Gain'' (which generally increases the noise). And as the absolutely last resort we reluctantly increase the ''Green Gain'', but only if **all** the following conditions are true:
- We have determined that to reach the target luminosity we need to increase the brightness at least 4-fold
- AWB is **on**, since the color representation is going to be severely affected by this change
- We have sufficient buffer for Red and Blue channels' Gain increases
void AE::_aeInc()
{
const auto limitExp = m_ov.getLimit(Val::exposure);
const int setExp = m_ov[Val::exposure];
if(setExp < limitExp){
m_ov[Val::exposure] = min(limitExp, static_cast((setExp ? setExp : 2000) * m_vars.adjY));
m_vars.op = CorrOp::inc_exp;
return;
}
const auto limitGg = m_ov.getLimit(Val::gain_global);
const int setGg = m_ov[Val::gain_global];
if(setGg < limitGg){
m_ov[Val::gain_global] = min(limitGg, static_cast((setGg ? setGg : 10) * m_vars.adjY));
m_vars.op = CorrOp::inc_gl;
return;
}
const auto limitRgb = m_ov.getLimit(Val::gain_g);
const int setGreen = m_ov[Val::gain_g];
if(m_vars.adjY > 4 && setGreen < limitRgb * 0.95){
const auto safetyBuffer = 0.95;
const auto limitRGain = 1180;
const int highestRgb = max(m_ov[Val::gain_r], m_ov[Val::gain_b]);
const double topMultiplier = std::clamp(m_vars.adjY, 1., safetyBuffer * limitRgb / highestRgb);
const auto gainG = static_cast(min(limitRGain, setGreen * topMultiplier));
if(gainG > setGreen){
m_ov[Val::gain_g] = gainG;
m_vars.op = CorrOp::inc_green;
return;
}
}
m_vars.op = CorrOp::noop;
}
===== AE::_aeDec() =====
Quite expectedly this function mirrors ''AE::_aeInc()''. First it tries to bring the ''Green Gain'' back to its default level of ''1024'', then reduce the ''Global Gain'' and, lastly, the ''Exposure'':
void AE::_aeDec()
{
const int setGreen = m_ov[Val::gain_g];
if(setGreen > 1024){
const auto nextGreen = static_cast(setGreen - (setGreen - 1024) * 10_pcnt);
assert(nextGreen >= 1024);
m_ov[Val::gain_g] = nextGreen;
m_vars.op = CorrOp::dec_green;
return;
}
const int setGg = m_ov[Val::gain_global];
if(setGg > 0){
m_ov[Val::gain_global] = static_cast(setGg * m_vars.adjY);
m_vars.op = CorrOp::dec_gl;
}else{
m_ov[Val::exposure] *= m_vars.adjY;
m_vars.op = CorrOp::dec_exp;
if(m_ov[Val::exposure] < 10){ // safety net to make sure we never get to corner case
m_ov[Val::exposure] = 10;
m_vars.op = CorrOp::noop;
}
}
}
====== Sample usage ======
This is a copy-paste of the code used in [[manual:Aria]] utility for the POC. This code has minimal comments but should be fairly easy to follow:
namespace{
static CString _corrOp2Txt(AE::CorrOp _op){
using op = AE::CorrOp;
switch(_op){
case op::noop: return {};
case op::inc_exp: return L"+++ Increasing Exposure";
case op::inc_gl: return L"+++ Increasing GLOBAL gain";
case op::inc_green: return L"+++ PANIC! Increasing GREEN gain";
case op::dec_exp: return L"--- Reducing Exposure";
case op::dec_gl: return L"--- Reducing GLOBAL gain";
case op::dec_green: return L"--- Reducing GREEN gain";
}
return {};
}
}
void ImgAnalyzerDlg::_aeInfo()
{
if(((CButton *)(GetDlgItem(IDC_CHECK_ISA_AE)))->GetCheck() != BST_CHECKED){
return;
}
const auto & stats = m_analyzer.getStats();
const auto correct = ((CButton*)(GetDlgItem(IDC_CHECK_ISA_AE_CORRECT)))->GetCheck() == BST_CHECKED;
const auto aeTolerance = PerPart(GetDlgItemInt(IDC_EDIT_ISA_AE_RIGHT, NULL, FALSE), 1000);
const int aeTgtLuma = GetDlgItemInt(IDC_EDIT_ISA_AE_BRIGHTNESS, NULL, FALSE);
const PerPart aeHysteresis{GetDlgItemInt(IDC_EDIT_ISA_AE_HYSTERESIS, NULL, FALSE), 100};
AE ae{m_fx3.sensor(), stats, aeTolerance, aeTgtLuma, aeHysteresis};
const auto rez = ae(correct);
// display the information
{
const PerPart pxUsed{stats.ae.pixels, stats.total.pixels};
CString msg;
msg.AppendFormat(L"Pixels used: %d (%.1f%%)\n", stats.ae.pixels, pxUsed * 100);
msg.AppendFormat(L"Under/over-exposed: %d/%d\n", rez.underExposed, rez.overExposed);
msg.AppendFormat(L"BL adj.: %d\n", rez.adjBL);
msg.AppendFormat(L"Avg Y.: %d\n", rez.avgY);
msg.AppendFormat(L"Brightness adj.: %.2f\n", rez.adjY);
msg += _corrOp2Txt(ae.details().op);
SetDlgItemTextW(IDC_STATIC_ISA_AE, msg);
}
}
====== Initial values ======
Following are the initial defaults for the variables that affect the process of automatic image brightness (Black Level, Exposure, Gains) adjustment:
^ Name (as seen in ''%%ImgAnalyzerDlg%%::_aeInfo()'' above) ^ Value ^
| ''aeTolerance'' | ''1/1000'' |
| ''aeTgtLuma'' | ''90'' |
| ''aeHysteresis'' | ''5/100'' |
| "center window" dimensions | ''50%'' vertically and horizontally, centered at image's center |