======= 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 |