The factor used right now in the IPU3 is 8192, as a multiplier of the estimated gain. This is wrong, as the isp is adding 1.0 to the gain applied, ie Pout = { Pin * (1 + Gx) }. Fix it, and to ease the reading, introduce a small helper function. Signed-off-by: Jean-Michel Hautbois <jeanmichel.hautbois@ideasonboard.com> Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
488 lines
17 KiB
C++
488 lines
17 KiB
C++
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
|
/*
|
|
* Copyright (C) 2021, Ideas On Board
|
|
*
|
|
* awb.cpp - AWB control algorithm
|
|
*/
|
|
#include "awb.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
#include <libcamera/base/log.h>
|
|
|
|
/**
|
|
* \file awb.h
|
|
*/
|
|
|
|
namespace libcamera {
|
|
|
|
namespace ipa::ipu3::algorithms {
|
|
|
|
LOG_DEFINE_CATEGORY(IPU3Awb)
|
|
|
|
/*
|
|
* When zones are used for the grey world algorithm, they are only considered if
|
|
* their average green value is at least 16/255 (after black level subtraction)
|
|
* to exclude zones that are too dark and don't provide relevant colour
|
|
* information (on the opposite side of the spectrum, saturated regions are
|
|
* excluded by the ImgU statistics engine).
|
|
*/
|
|
static constexpr uint32_t kMinGreenLevelInZone = 16;
|
|
|
|
/*
|
|
* Minimum proportion of non-saturated cells in a zone for the zone to be used
|
|
* by the AWB algorithm.
|
|
*/
|
|
static constexpr double kMaxCellSaturationRatio = 0.8;
|
|
|
|
/*
|
|
* Maximum ratio of saturated pixels in a cell for the cell to be considered
|
|
* non-saturated and counted by the AWB algorithm.
|
|
*/
|
|
static constexpr uint32_t kMinCellsPerZoneRatio = 255 * 90 / 100;
|
|
|
|
/**
|
|
* \struct Accumulator
|
|
* \brief RGB statistics for a given zone
|
|
*
|
|
* Accumulate red, green and blue values for each non-saturated item over a
|
|
* zone. Items can for instance be pixels, but also the average of groups of
|
|
* pixels, depending on who uses the accumulator.
|
|
* \todo move this description and structure into a common header
|
|
*
|
|
* Zones which are saturated beyond the threshold defined in
|
|
* ipu3_uapi_awb_config_s are not included in the average.
|
|
*
|
|
* \var Accumulator::counted
|
|
* \brief Number of unsaturated cells used to calculate the sums
|
|
*
|
|
* \var Accumulator::sum
|
|
* \brief A structure containing the average red, green and blue sums
|
|
*
|
|
* \var Accumulator::sum.red
|
|
* \brief Sum of the average red values of each unsaturated cell in the zone
|
|
*
|
|
* \var Accumulator::sum.green
|
|
* \brief Sum of the average green values of each unsaturated cell in the zone
|
|
*
|
|
* \var Accumulator::sum.blue
|
|
* \brief Sum of the average blue values of each unsaturated cell in the zone
|
|
*/
|
|
|
|
/**
|
|
* \struct Awb::AwbStatus
|
|
* \brief AWB parameters calculated
|
|
*
|
|
* The AwbStatus structure is intended to store the AWB
|
|
* parameters calculated by the algorithm
|
|
*
|
|
* \var AwbStatus::temperatureK
|
|
* \brief Color temperature calculated
|
|
*
|
|
* \var AwbStatus::redGain
|
|
* \brief Gain calculated for the red channel
|
|
*
|
|
* \var AwbStatus::greenGain
|
|
* \brief Gain calculated for the green channel
|
|
*
|
|
* \var AwbStatus::blueGain
|
|
* \brief Gain calculated for the blue channel
|
|
*/
|
|
|
|
/* Default settings for Bayer noise reduction replicated from the Kernel */
|
|
static const struct ipu3_uapi_bnr_static_config imguCssBnrDefaults = {
|
|
.wb_gains = { 16, 16, 16, 16 },
|
|
.wb_gains_thr = { 255, 255, 255, 255 },
|
|
.thr_coeffs = { 1700, 0, 31, 31, 0, 16 },
|
|
.thr_ctrl_shd = { 26, 26, 26, 26 },
|
|
.opt_center = { -648, 0, -366, 0 },
|
|
.lut = {
|
|
{ 17, 23, 28, 32, 36, 39, 42, 45,
|
|
48, 51, 53, 55, 58, 60, 62, 64,
|
|
66, 68, 70, 72, 73, 75, 77, 78,
|
|
80, 82, 83, 85, 86, 88, 89, 90 } },
|
|
.bp_ctrl = { 20, 0, 1, 40, 0, 6, 0, 6, 0 },
|
|
.dn_detect_ctrl = { 9, 3, 4, 0, 8, 0, 1, 1, 1, 1, 0 },
|
|
.column_size = 1296,
|
|
.opt_center_sqr = { 419904, 133956 },
|
|
};
|
|
|
|
/* Default color correction matrix defined as an identity matrix */
|
|
static const struct ipu3_uapi_ccm_mat_config imguCssCcmDefault = {
|
|
8191, 0, 0, 0,
|
|
0, 8191, 0, 0,
|
|
0, 0, 8191, 0
|
|
};
|
|
|
|
/**
|
|
* \class Awb
|
|
* \brief A Grey world white balance correction algorithm
|
|
*
|
|
* The Grey World algorithm assumes that the scene, in average, is neutral grey.
|
|
* Reference: Lam, Edmund & Fung, George. (2008). Automatic White Balancing in
|
|
* Digital Photography. 10.1201/9781420054538.ch10.
|
|
*
|
|
* The IPU3 generates statistics from the Bayer Down Scaler output into a grid
|
|
* defined in the ipu3_uapi_awb_config_s structure.
|
|
*
|
|
* - Cells are defined in Pixels
|
|
* - Zones are defined in Cells
|
|
*
|
|
* 80 cells
|
|
* /───────────── 1280 pixels ───────────\
|
|
* 16 zones
|
|
* 16
|
|
* ┌────┬────┬────┬────┬────┬─ ──────┬────┐ \
|
|
* │Cell│ │ │ │ │ | │ │ │
|
|
* 16 │ px │ │ │ │ │ | │ │ │
|
|
* ├────┼────┼────┼────┼────┼─ ──────┼────┤ │
|
|
* │ │ │ │ │ │ | │ │
|
|
* │ │ │ │ │ │ | │ │ 7
|
|
* │ ── │ ── │ ── │ ── │ ── │ ── ── ─┤ ── │ 1 2 4
|
|
* │ │ │ │ │ │ | │ │ 2 0 5
|
|
*
|
|
* │ │ │ │ │ │ | │ │ z p c
|
|
* ├────┼────┼────┼────┼────┼─ ──────┼────┤ o i e
|
|
* │ │ │ │ │ │ | │ │ n x l
|
|
* │ │ | │ │ e e l
|
|
* ├─── ───┼─ ──────┼────┤ s l s
|
|
* │ │ | │ │ s
|
|
* │ │ | │ │
|
|
* ├─── Zone of Cells ───┼─ ──────┼────┤ │
|
|
* │ (5 x 4) │ | │ │ │
|
|
* │ │ | │ │ │
|
|
* ├── ───┼─ ──────┼────┤ │
|
|
* │ │ │ | │ │ │
|
|
* │ │ │ │ │ │ | │ │ │
|
|
* └────┴────┴────┴────┴────┴─ ──────┴────┘ /
|
|
*
|
|
*
|
|
* In each cell, the ImgU computes for each colour component the average of all
|
|
* unsaturated pixels (below a programmable threshold). It also provides the
|
|
* ratio of saturated pixels in the cell.
|
|
*
|
|
* The AWB algorithm operates on a coarser grid, made by grouping cells from the
|
|
* hardware grid into zones. The number of zones is fixed to \a kAwbStatsSizeX x
|
|
* \a kAwbStatsSizeY. For example, a frame of 1280x720 is divided into 80x45
|
|
* cells of [16x16] pixels and 16x12 zones of [5x4] cells each
|
|
* (\a kAwbStatsSizeX=16 and \a kAwbStatsSizeY=12). If the number of cells isn't
|
|
* an exact multiple of the number of zones, the right-most and bottom-most
|
|
* cells are ignored. The grid configuration is computed by
|
|
* IPAIPU3::calculateBdsGrid().
|
|
*
|
|
* Before calculating the gains, the algorithm aggregates the cell averages for
|
|
* each zone in generateAwbStats(). Cells that have a too high ratio of
|
|
* saturated pixels are ignored, and only zones that contain enough
|
|
* non-saturated cells are then used by the algorithm.
|
|
*
|
|
* The Grey World algorithm will then estimate the red and blue gains to apply, and
|
|
* store the results in the metadata. The green gain is always set to 1.
|
|
*/
|
|
|
|
Awb::Awb()
|
|
: Algorithm()
|
|
{
|
|
asyncResults_.blueGain = 1.0;
|
|
asyncResults_.greenGain = 1.0;
|
|
asyncResults_.redGain = 1.0;
|
|
asyncResults_.temperatureK = 4500;
|
|
|
|
zones_.reserve(kAwbStatsSizeX * kAwbStatsSizeY);
|
|
}
|
|
|
|
Awb::~Awb() = default;
|
|
|
|
/**
|
|
* \copydoc libcamera::ipa::Algorithm::configure
|
|
*/
|
|
int Awb::configure(IPAContext &context,
|
|
[[maybe_unused]] const IPAConfigInfo &configInfo)
|
|
{
|
|
const ipu3_uapi_grid_config &grid = context.configuration.grid.bdsGrid;
|
|
stride_ = context.configuration.grid.stride;
|
|
|
|
cellsPerZoneX_ = std::round(grid.width / static_cast<double>(kAwbStatsSizeX));
|
|
cellsPerZoneY_ = std::round(grid.height / static_cast<double>(kAwbStatsSizeY));
|
|
|
|
/*
|
|
* Configure the minimum proportion of cells counted within a zone
|
|
* for it to be relevant for the grey world algorithm.
|
|
* \todo This proportion could be configured.
|
|
*/
|
|
cellsPerZoneThreshold_ = cellsPerZoneX_ * cellsPerZoneY_ * kMaxCellSaturationRatio;
|
|
LOG(IPU3Awb, Debug) << "Threshold for AWB is set to " << cellsPerZoneThreshold_;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* The function estimates the correlated color temperature using
|
|
* from RGB color space input.
|
|
* In physics and color science, the Planckian locus or black body locus is
|
|
* the path or locus that the color of an incandescent black body would take
|
|
* in a particular chromaticity space as the blackbody temperature changes.
|
|
*
|
|
* If a narrow range of color temperatures is considered (those encapsulating
|
|
* daylight being the most practical case) one can approximate the Planckian
|
|
* locus in order to calculate the CCT in terms of chromaticity coordinates.
|
|
*
|
|
* More detailed information can be found in:
|
|
* https://en.wikipedia.org/wiki/Color_temperature#Approximation
|
|
*/
|
|
uint32_t Awb::estimateCCT(double red, double green, double blue)
|
|
{
|
|
/* Convert the RGB values to CIE tristimulus values (XYZ) */
|
|
double X = (-0.14282) * (red) + (1.54924) * (green) + (-0.95641) * (blue);
|
|
double Y = (-0.32466) * (red) + (1.57837) * (green) + (-0.73191) * (blue);
|
|
double Z = (-0.68202) * (red) + (0.77073) * (green) + (0.56332) * (blue);
|
|
|
|
/* Calculate the normalized chromaticity values */
|
|
double x = X / (X + Y + Z);
|
|
double y = Y / (X + Y + Z);
|
|
|
|
/* Calculate CCT */
|
|
double n = (x - 0.3320) / (0.1858 - y);
|
|
return 449 * n * n * n + 3525 * n * n + 6823.3 * n + 5520.33;
|
|
}
|
|
|
|
/* Generate an RGB vector with the average values for each zone */
|
|
void Awb::generateZones()
|
|
{
|
|
zones_.clear();
|
|
|
|
for (unsigned int i = 0; i < kAwbStatsSizeX * kAwbStatsSizeY; i++) {
|
|
RGB zone;
|
|
double counted = awbStats_[i].counted;
|
|
if (counted >= cellsPerZoneThreshold_) {
|
|
zone.G = awbStats_[i].sum.green / counted;
|
|
if (zone.G >= kMinGreenLevelInZone) {
|
|
zone.R = awbStats_[i].sum.red / counted;
|
|
zone.B = awbStats_[i].sum.blue / counted;
|
|
zones_.push_back(zone);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Translate the IPU3 statistics into the default statistics zone array */
|
|
void Awb::generateAwbStats(const ipu3_uapi_stats_3a *stats)
|
|
{
|
|
/*
|
|
* Generate a (kAwbStatsSizeX x kAwbStatsSizeY) array from the IPU3 grid which is
|
|
* (grid.width x grid.height).
|
|
*/
|
|
for (unsigned int cellY = 0; cellY < kAwbStatsSizeY * cellsPerZoneY_; cellY++) {
|
|
for (unsigned int cellX = 0; cellX < kAwbStatsSizeX * cellsPerZoneX_; cellX++) {
|
|
uint32_t cellPosition = cellY * stride_ + cellX;
|
|
uint32_t zoneX = cellX / cellsPerZoneX_;
|
|
uint32_t zoneY = cellY / cellsPerZoneY_;
|
|
|
|
uint32_t awbZonePosition = zoneY * kAwbStatsSizeX + zoneX;
|
|
|
|
/* Cast the initial IPU3 structure to simplify the reading */
|
|
const ipu3_uapi_awb_set_item *currentCell =
|
|
reinterpret_cast<const ipu3_uapi_awb_set_item *>(
|
|
&stats->awb_raw_buffer.meta_data[cellPosition]
|
|
);
|
|
|
|
/*
|
|
* Use cells which have less than 90%
|
|
* saturation as an initial means to include
|
|
* otherwise bright cells which are not fully
|
|
* saturated.
|
|
*
|
|
* \todo The 90% saturation rate may require
|
|
* further empirical measurements and
|
|
* optimisation during camera tuning phases.
|
|
*/
|
|
if (currentCell->sat_ratio <= kMinCellsPerZoneRatio) {
|
|
/* The cell is not saturated, use the current cell */
|
|
awbStats_[awbZonePosition].counted++;
|
|
uint32_t greenValue = currentCell->Gr_avg + currentCell->Gb_avg;
|
|
awbStats_[awbZonePosition].sum.green += greenValue / 2;
|
|
awbStats_[awbZonePosition].sum.red += currentCell->R_avg;
|
|
awbStats_[awbZonePosition].sum.blue += currentCell->B_avg;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Awb::clearAwbStats()
|
|
{
|
|
for (unsigned int i = 0; i < kAwbStatsSizeX * kAwbStatsSizeY; i++) {
|
|
awbStats_[i].sum.blue = 0;
|
|
awbStats_[i].sum.red = 0;
|
|
awbStats_[i].sum.green = 0;
|
|
awbStats_[i].counted = 0;
|
|
}
|
|
}
|
|
|
|
void Awb::awbGreyWorld()
|
|
{
|
|
LOG(IPU3Awb, Debug) << "Grey world AWB";
|
|
/*
|
|
* Make a separate list of the derivatives for each of red and blue, so
|
|
* that we can sort them to exclude the extreme gains. We could
|
|
* consider some variations, such as normalising all the zones first, or
|
|
* doing an L2 average etc.
|
|
*/
|
|
std::vector<RGB> &redDerivative(zones_);
|
|
std::vector<RGB> blueDerivative(redDerivative);
|
|
std::sort(redDerivative.begin(), redDerivative.end(),
|
|
[](RGB const &a, RGB const &b) {
|
|
return a.G * b.R < b.G * a.R;
|
|
});
|
|
std::sort(blueDerivative.begin(), blueDerivative.end(),
|
|
[](RGB const &a, RGB const &b) {
|
|
return a.G * b.B < b.G * a.B;
|
|
});
|
|
|
|
/* Average the middle half of the values. */
|
|
int discard = redDerivative.size() / 4;
|
|
|
|
RGB sumRed(0, 0, 0);
|
|
RGB sumBlue(0, 0, 0);
|
|
for (auto ri = redDerivative.begin() + discard,
|
|
bi = blueDerivative.begin() + discard;
|
|
ri != redDerivative.end() - discard; ri++, bi++)
|
|
sumRed += *ri, sumBlue += *bi;
|
|
|
|
double redGain = sumRed.G / (sumRed.R + 1),
|
|
blueGain = sumBlue.G / (sumBlue.B + 1);
|
|
|
|
/* Color temperature is not relevant in Grey world but still useful to estimate it :-) */
|
|
asyncResults_.temperatureK = estimateCCT(sumRed.R, sumRed.G, sumBlue.B);
|
|
|
|
/*
|
|
* Gain values are unsigned integer value ranging [0, 8) with 13 bit
|
|
* fractional part.
|
|
*/
|
|
redGain = std::clamp(redGain, 0.0, 65535.0 / 8192);
|
|
blueGain = std::clamp(blueGain, 0.0, 65535.0 / 8192);
|
|
|
|
asyncResults_.redGain = redGain;
|
|
/* Hardcode the green gain to 1.0. */
|
|
asyncResults_.greenGain = 1.0;
|
|
asyncResults_.blueGain = blueGain;
|
|
}
|
|
|
|
void Awb::calculateWBGains(const ipu3_uapi_stats_3a *stats)
|
|
{
|
|
ASSERT(stats->stats_3a_status.awb_en);
|
|
|
|
clearAwbStats();
|
|
generateAwbStats(stats);
|
|
generateZones();
|
|
|
|
LOG(IPU3Awb, Debug) << "Valid zones: " << zones_.size();
|
|
|
|
if (zones_.size() > 10) {
|
|
awbGreyWorld();
|
|
LOG(IPU3Awb, Debug) << "Gain found for red: " << asyncResults_.redGain
|
|
<< " and for blue: " << asyncResults_.blueGain;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \copydoc libcamera::ipa::Algorithm::process
|
|
*/
|
|
void Awb::process(IPAContext &context, [[maybe_unused]] IPAFrameContext *frameContext,
|
|
const ipu3_uapi_stats_3a *stats)
|
|
{
|
|
calculateWBGains(stats);
|
|
|
|
/*
|
|
* Gains are only recalculated if enough zones were detected.
|
|
* The results are cached, so if no results were calculated, we set the
|
|
* cached values from asyncResults_ here.
|
|
*/
|
|
context.activeState.awb.gains.blue = asyncResults_.blueGain;
|
|
context.activeState.awb.gains.green = asyncResults_.greenGain;
|
|
context.activeState.awb.gains.red = asyncResults_.redGain;
|
|
context.activeState.awb.temperatureK = asyncResults_.temperatureK;
|
|
}
|
|
|
|
constexpr uint16_t Awb::threshold(float value)
|
|
{
|
|
/* AWB thresholds are in the range [0, 8191] */
|
|
return value * 8191;
|
|
}
|
|
|
|
constexpr uint16_t Awb::gainValue(double gain)
|
|
{
|
|
/*
|
|
* The colour gains applied by the BNR for the four channels (Gr, R, B
|
|
* and Gb) are expressed in the parameters structure as 16-bit integers
|
|
* that store a fixed-point U3.13 value in the range [0, 8[.
|
|
*
|
|
* The real gain value is equal to the gain parameter plus one, i.e.
|
|
*
|
|
* Pout = Pin * (1 + gain / 8192)
|
|
*
|
|
* where 'Pin' is the input pixel value, 'Pout' the output pixel value,
|
|
* and 'gain' the gain in the parameters structure as a 16-bit integer.
|
|
*/
|
|
return std::clamp((gain - 1.0) * 8192, 0.0, 65535.0);
|
|
}
|
|
|
|
/**
|
|
* \copydoc libcamera::ipa::Algorithm::prepare
|
|
*/
|
|
void Awb::prepare(IPAContext &context, ipu3_uapi_params *params)
|
|
{
|
|
/*
|
|
* Green saturation thresholds are reduced because we are using the
|
|
* green channel only in the exposure computation.
|
|
*/
|
|
params->acc_param.awb.config.rgbs_thr_r = threshold(1.0);
|
|
params->acc_param.awb.config.rgbs_thr_gr = threshold(0.9);
|
|
params->acc_param.awb.config.rgbs_thr_gb = threshold(0.9);
|
|
params->acc_param.awb.config.rgbs_thr_b = threshold(1.0);
|
|
|
|
/*
|
|
* Enable saturation inclusion on thr_b for ImgU to update the
|
|
* ipu3_uapi_awb_set_item->sat_ratio field.
|
|
*/
|
|
params->acc_param.awb.config.rgbs_thr_b |= IPU3_UAPI_AWB_RGBS_THR_B_INCL_SAT |
|
|
IPU3_UAPI_AWB_RGBS_THR_B_EN;
|
|
|
|
const ipu3_uapi_grid_config &grid = context.configuration.grid.bdsGrid;
|
|
|
|
params->acc_param.awb.config.grid = context.configuration.grid.bdsGrid;
|
|
|
|
/*
|
|
* Optical center is column start (respectively row start) of the
|
|
* cell of interest minus its X center (respectively Y center).
|
|
*
|
|
* For the moment use BDS as a first approximation, but it should
|
|
* be calculated based on Shading (SHD) parameters.
|
|
*/
|
|
params->acc_param.bnr = imguCssBnrDefaults;
|
|
Size &bdsOutputSize = context.configuration.grid.bdsOutputSize;
|
|
params->acc_param.bnr.column_size = bdsOutputSize.width;
|
|
params->acc_param.bnr.opt_center.x_reset = grid.x_start - (bdsOutputSize.width / 2);
|
|
params->acc_param.bnr.opt_center.y_reset = grid.y_start - (bdsOutputSize.height / 2);
|
|
params->acc_param.bnr.opt_center_sqr.x_sqr_reset = params->acc_param.bnr.opt_center.x_reset
|
|
* params->acc_param.bnr.opt_center.x_reset;
|
|
params->acc_param.bnr.opt_center_sqr.y_sqr_reset = params->acc_param.bnr.opt_center.y_reset
|
|
* params->acc_param.bnr.opt_center.y_reset;
|
|
|
|
params->acc_param.bnr.wb_gains.gr = gainValue(context.activeState.awb.gains.green);
|
|
params->acc_param.bnr.wb_gains.r = gainValue(context.activeState.awb.gains.red);
|
|
params->acc_param.bnr.wb_gains.b = gainValue(context.activeState.awb.gains.blue);
|
|
params->acc_param.bnr.wb_gains.gb = gainValue(context.activeState.awb.gains.green);
|
|
|
|
LOG(IPU3Awb, Debug) << "Color temperature estimated: " << asyncResults_.temperatureK;
|
|
|
|
/* The CCM matrix may change when color temperature will be used */
|
|
params->acc_param.ccm = imguCssCcmDefault;
|
|
|
|
params->use.acc_awb = 1;
|
|
params->use.acc_bnr = 1;
|
|
params->use.acc_ccm = 1;
|
|
}
|
|
|
|
} /* namespace ipa::ipu3::algorithms */
|
|
|
|
} /* namespace libcamera */
|