User Tools

This is an old revision of the document!


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 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 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.total.pixels * _pp);
    for(int i = 0; i < 256; ++i){
        count -= _stats.hist[HIST_Y][i];
        if(count < 0){
            return i;
        }
    }
    return 255;
}
 
int _triggeredRight(const ImgStats& _stats, PerPart _pp){
    auto count = max(defectivePixels, _stats.total.pixels * _pp);
    for(int i = 255; i >= 0; --i){
        count -= _stats.hist[HIST_Y][i];
        if(count < 0){
            return i;
        }
    }
    return 0;
}
 
}

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:

  1. Instantiate an object
  2. Call its functor
  3. (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 ImgStats & m_stats;       // frame's statistical data
 
    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)
    : m_ov(*_ov)
    , m_stats(_stats)
{
    m_vars.underExposed = _triggeredLeft(m_stats, _pp);
    m_vars.overExposed = _triggeredRight(m_stats, _pp);
    m_vars.avgY = static_cast<int>(m_stats.total.pixels == 0 ? 0 : m_stats.total.y / m_stats.total.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_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_vars.actionRH = Action::dec;  // too many overexposed pixels and NOT increasing brightness
    }else if(m_vars.overExposed < 240 && m_vars.adjY > 1){
        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:

  1. We have determined that to reach the target luminosity we need to increase the brightness at least 4-fold
  2. AWB is on, since the color representation is going to be severely affected by this change
  3. 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<int>((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<int>((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<int>(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<int>(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<int>(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;
        }
    }
}

This website uses cookies. By using the website, you agree with storing cookies on your computer. Also, you acknowledge that you have read and understand our Privacy Policy. If you do not agree, please leave the website.

More information