User Tools

Site Tools


isp:sample_awb_implementation_in_c

Sample AWB implementation in C++

The code below provides a sample implementation of the AWB algorithm described in Automatic white balance. 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::LumaCalc - a parameter that affects the choice of algorithm for RGBYUV1) conversions
  • 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.

Helper one-liners

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).

Side note: I'm fully aware of the std::clamp() template yet using my own _box() because of type conversions I need. In one case I'm limiting a double into an int range, returning an int result. In the second case the range is specified by PerPart values that would otherwise need to be explicitly cast to double which looked too ugly, TBH.

namespace{
 
// "box" a given value _x between two bounding limits _l and _r, both inclusive
constexpr int _box(long double _x, int _l, int _r) { return static_cast<int>(max(_l, min(_r, round(_x)))); }
constexpr auto _box(long double _x, long double _l, long double _r) { return max(_l, min(_r, _x)); }
// sum-up the values of the _count right-most elements of a histogram (assumed to be of 256 elements)
auto _histRightNSum(const long * _el, int _count) { return std::accumulate(_el + 256 - _count, _el + 256, 0); }
 
// implementation not shown - basically just a matrix multiplication
struct RGB_DBL{ double r, g, b; };
constexpr RGB_DBL _RGB(LumaCalc _algo, double _y, double _u, double _v);
}

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<long double>(_val) / _div)
    {}
    operator long double() const { return m_val; }
    int pcnt() const { return static_cast<int>(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);}
 
}

AWB

Main auto-white balance 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
  4. (Optionally) adjust the “saturation threshold”, based on the recommendation from the analysis

The “saturation threshold” affects data collection in such a way that only those pixels for which the statement (|U| + |V|) / Y < threshold holds true, i.e. only pixels that are “grey enough” are considered for the data set and those which are way too saturated are ignored.

Declaration

struct AWB{
    enum class Action{noop, adjTolerance, correctGains, resetRed};
 
    const PerPart m_satTH;      // threshold, max saturation of pixels to consider them "grey enough", [0..1]
    const long long m_pxUsed;   // number of pixels in the stats
 
    AWB(IImgSensor * _ov, LumaCalc _alg, const ImgStats & _stats, const PerPart & _satTH);
 
    auto operator()(bool _performCorrection)  // analyze and, if asked and is necessary, adjust sensor's config
    {
        _analyze();
        if(_performCorrection){
            switch(m_vars.op){
            case Action::correctGains: _correctGains(); break;
            case Action::resetRed:     _resetRed();     break;
            }
        }
        return m_vars;
    }
 
    const auto & details() const {return m_vars;}   // diagnostics to display
 
private:
    void _analyze();                // analyze data
    bool _isRedOE() const;          // true if there's way too much red
    void _resetRed();               // bring Red channel in line with Green if the picture is way too red
    void _correctGains();           // perform the gains' adjustment to normalize colors on the output
    void _adjTol(long double _adj); // adjust tolerance level to make m_rezA.pxCount into m_sweetZone range
 
private:
    using Val = IImgSensor::Value;  // avoid typing out the full type qualifier
    IImgSensor & m_ov;              // OmniVision sensor interface
 
    const struct{ PerPart low, high; }
      m_tgtPxUsed = {16_pcnt, 35_pcnt}
    , m_tgtSatTH  = {1_pcnt, 60_pcnt};
 
    struct{
        PerPart pxPcnt = 0_pcnt;    // pixels contributing to statistics
        double avgY = 0;
        double avgU = 0;
        double avgV = 0;
        double avgR = 0;
        double avgG = 0;
        double avgB = 0;
        int oeR = 0;                // overxposed Red channel pixels
        int oeG = 0;                // overexposed Green channel pixels
        PerPart suggTolerance = 0;  // suggested tolerance, based on m_tolerance
        Action op = Action::noop;   // suggested action to perform
    } m_vars;   // useful results of calculations
};

Constructor AWB::AWB

A bulk of (quite simplistic) calculations is performed with data in _stats and the results are stored in the m_vars struct:

AWB::AWB(IImgSensor * _ov, LumaCalc _alg, const ImgStats & _stats, const PerPart & _satTH)
    : m_ov(*_ov)
    , m_pxUsed(_stats.awb.pxUsed)
    , m_satTH(_satTH)
{
    m_vars.pxPcnt = PerPart(m_pxUsed, _stats.awb.pxTotal);
    m_vars.oeR = _histRightNSum(_stats.hist[HIST_R], 25);
    m_vars.oeG = _histRightNSum(_stats.hist[HIST_G], 25);
    if(m_pxUsed > 0){
        m_vars.avgY = 1. * _stats.awb.y / m_pxUsed;
        m_vars.avgU = 1. * _stats.awb.u / m_pxUsed;
        m_vars.avgV = 1. * _stats.awb.v / m_pxUsed;
        // _alg is either BT.601 or BT.709 for RGB<->Y'UV, must match what we used to gather stats
        const auto rgb = _RGB(_alg, m_vars.avgY, m_vars.avgU, m_vars.avgV);
        m_vars.avgR = rgb.r;
        m_vars.avgG = rgb.g;
        m_vars.avgB = rgb.b;
    }
}

AWB::_analyze()

Perform the analysis of available data and come up with a recommendation on how to adjust the sensor, if necessary.

void AWB::_analyze()
{
    m_vars.op = Action::correctGains;
    if(_isRedOE()){
        m_vars.op = Action::resetRed;
    }else if(m_vars.pxPcnt < m_tgtPxUsed.low && m_satTH < m_tgtSatTH.high){
        m_vars.op = Action::adjTolerance;
        _adjTol(m_tgtPxUsed.low - m_vars.pxPcnt + 1_pcnt);
    }else if(m_vars.pxPcnt > m_tgtPxUsed.high && m_satTH > m_tgtSatTH.low){
        m_vars.op = Action::adjTolerance;
        _adjTol(m_tgtPxUsed.high - m_vars.pxPcnt - 1_pcnt);
    }
}

AWB::_isRedOE()

Figure out if the red channel is way hot.

// special conditions: way too little light and way too much green that "looks like grey" due
// to red channel over-exposure caused by red gain, which is noticeable by the huge spike of
// overexposed pixels in the red channel's histogram
// A more generic definition of this special condition is: too much red overexposure relative to
// green overexposure relative to overall brightness of the picture. The brighter the image the
// more overexposure we can tolerate, but to a certain point
bool AWB::_isRedOE() const
{
    if(m_vars.oeR < m_pxUsed * 0.15 && m_vars.oeG < m_pxUsed * 0.01){
        return false;
    }
    if(m_vars.avgY < 30){
        return m_vars.oeR > m_vars.oeG;
    }else if(m_vars.avgY < 60){
        return m_vars.oeR > m_vars.oeG * m_vars.avgY * 3;
    }else if(m_vars.avgY < 85){
        return m_vars.oeR > m_vars.oeG * m_vars.avgY;
    }else{
        return m_vars.oeR > m_vars.oeG * 8;
    }
}

AWB::_resetRed()

If the red channel is way hot what we do is bring it almost half-way back to where the green gain is (62.5% to be precise, but that number is just a convenient “slightly bigger than half” and easy to express as a simple fraction).

void AWB::_resetRed()
{
    const int gainG = m_ov[Val::gain_g];
    const int gainR = m_ov[Val::gain_r];
    if(gainR > gainG){
        const auto diff = (gainG - gainR) * 5 / 8;  // 62.5% closer to green gain value
        m_ov[Val::gain_r] = gainR + diff;
    }
}

AWB::_correctGains()

Once we've determined that we should correct the gains this function is called to do just that. Both Red and Blue channels are corrected together, which may produce an oscillation effect and if that is observed when implemented in the hardware we should change this to only correct one channel or the other at a time, but not both.

The channel's Gain correction is performed if it's corresponding chroma component's “global average” value (U for Blue, V for Red) is more than precision further from 0.

void AWB::_correctGains()
{
    assert(m_vars.op == Action::correctGains);
 
    const auto limit = m_ov.getLimit(Val::gain_g); // assume the same limit for all 3 channels
    const double precision = 0.01;
 
    if(abs(m_vars.avgU) > precision){
        //// blue shift could just be caused by current post-processing - then this should be ignored
        //const double blueAdj = 0.98;
        //m_vars.avgB /= blueAdj;
        const auto gb = m_vars.avgB == 0 ? 0 : m_vars.avgG / m_vars.avgB;
        const int gainB = _box(m_ov[Val::gain_b] * gb, 10, limit);
        m_ov[Val::gain_b] = gainB;
    }
 
    if(abs(m_vars.avgV) > precision){
        const auto gr = m_vars.avgR == 0 ? 0 : m_vars.avgG / m_vars.avgR;
        const int gainR = _box(m_ov[Val::gain_r] * gr, 10, limit);
        m_ov[Val::gain_r] = gainR;
    }
}

AWB::_adjTol()

In cases when our stats collection yielded too little or too much data we need to adjust the “saturation tolerance” accordingly and try to bring the amount of collected data into a reasonable range. Experimentally we've determined that such range is [16..35]% which we try to reach by using a 'saturation tolerance“ in [1..60]% range.

void AWB::_adjTol(long double _adj)
{
    auto diff = _adj * m_satTH;    // adjust proportionally to current Saturation tolerance
    if(diff < 0){
        diff = min(-1_pcnt, diff);
    }else{
        diff = max(+1_pcnt, diff);
    }
    m_vars.suggTolerance = _box(m_satTH + diff, m_tgtSatTH.low, m_tgtSatTH.high);
}

Sample usage

This is a copy-paste of the code used in Aria utility for the POC. This code has minimal comments but should be fairly easy to follow:

void ImgAnalyzerDlg::_awbInfo()
{
    static long long counter = 0;   // skip a frame to catch up to the image in camera
    if(++counter % 2 == 0){
        return;
    }
    if(((CButton *)(GetDlgItem(IDC_CHECK_ISA_AWB)))->GetCheck() != BST_CHECKED){
        return;
    }
 
    const auto algo = m_ComboLumaCalc.GetCurSel() == 0 ? LumaCalc::BT601 : LumaCalc::BT709;
    const auto currTolerance = PerPart(GetDlgItemInt(IDC_EDIT_ISA_AWB_TOLERANCE, NULL, FALSE), 100);
    const auto & stats = m_analyzer.getStats();
    AWB awb{m_fx3.sensor(), algo, stats, currTolerance};
    const bool performCorrection = ((CButton *)(GetDlgItem(IDC_CHECK_ISA_AWB_CORRECT)))->GetCheck() == BST_CHECKED;
    const auto rez = awb(performCorrection);
    if(rez.op == AWB::Action::adjTolerance){
        SetDlgItemInt(IDC_EDIT_ISA_AWB_TOLERANCE, rez.suggTolerance.pcnt(), FALSE);
    }
 
    // display the information
    {
        CString msg;
        msg.AppendFormat(L"Set: %llu (%.1f%%)\n", stats.awb.pxUsed, rez.pxPcnt * 100);
        msg.AppendFormat(L"Avg. U, V: %.3f, %.3f\n", rez.avgU, rez.avgV);
        msg.AppendFormat(L"Avg. Y: %.1f\n", rez.avgY);
        msg.AppendFormat(L"Avg. R: %.1f\n", rez.avgR);
        msg.AppendFormat(L"Avg. G: %.1f\n", rez.avgG);
        msg.AppendFormat(L"Avg. B: %.1f\n", rez.avgB);
        msg.AppendFormat(L"R/G: %.4f\n", rez.avgG == 0 ? 0 : rez.avgR / rez.avgG);
        msg.AppendFormat(L"B/G: %.4f\n", rez.avgG == 0 ? 0 : rez.avgB / rez.avgG);
        msg.AppendFormat(L"oeR/oeG: %.5f\n", rez.oeG == 0 ? 0 : 1. * rez.oeR / rez.oeG);
        SetDlgItemTextW(IDC_STATIC_ISA_AWB, msg);
    }
}

Initial values

Following are the initial defaults for the variables that affect the process of automatic white balance adjustment:

Name (as seen in ImgAnalyzerDlg::_awbInfo() above) Value
currTolerance 15/100, adjusted automagically based on last frame's stats
1)
more precisely - a Y'UV
/home/adminsub2r/public_html/dokuwiki/data/pages/isp/sample_awb_implementation_in_c.txt · Last modified: 2018/07/03 22:21 by Igor Yefmov