mirror of
https://git.libcamera.org/libcamera/libcamera.git
synced 2025-07-16 08:55:06 +03:00
The lux value can never be negative. Pass it as an unsigned int. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Stefan Klug <stefan.klug@ideasonboard.com>
499 lines
14 KiB
C++
499 lines
14 KiB
C++
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
|
/*
|
|
* Copyright (C) 2019, Raspberry Pi Ltd
|
|
* Copyright (C) 2024 Ideas on Board Oy
|
|
*
|
|
* Implementation of a bayesian AWB algorithm
|
|
*/
|
|
|
|
#include "awb_bayes.h"
|
|
|
|
#include <cmath>
|
|
|
|
#include <libcamera/base/log.h>
|
|
#include <libcamera/control_ids.h>
|
|
|
|
#include "colours.h"
|
|
|
|
/**
|
|
* \file awb_bayes.h
|
|
* \brief Implementation of bayesian auto white balance algorithm
|
|
*
|
|
* This implementation is based on the initial implementation done by
|
|
* RaspberryPi.
|
|
* \todo: Documentation
|
|
*
|
|
* \todo Not all the features implemented by RaspberryPi were ported over to
|
|
* this algorithm because they either rely on hardware features not generally
|
|
* available or were considered not important enough at the moment.
|
|
*
|
|
* The following parts are not implemented:
|
|
*
|
|
* - min_pixels: minimum proportion of pixels counted within AWB region for it
|
|
* to be "useful"
|
|
* - min_g: minimum G value of those pixels, to be regarded a "useful"
|
|
* - min_regions: number of AWB regions that must be "useful" in order to do the
|
|
* AWB calculation
|
|
* - deltaLimit: clamp on colour error term (so as not to penalize non-grey
|
|
* excessively)
|
|
* - bias_proportion: The biasProportion parameter adds a small proportion of
|
|
* the counted pixels to a region biased to the biasCT colour temperature.
|
|
* A typical value for biasProportion would be between 0.05 to 0.1.
|
|
* - bias_ct: CT target for the search bias
|
|
* - sensitivityR: red sensitivity ratio (set to canonical sensor's R/G divided
|
|
* by this sensor's R/G)
|
|
* - sensitivityB: blue sensitivity ratio (set to canonical sensor's B/G divided
|
|
* by this sensor's B/G)
|
|
*/
|
|
|
|
namespace libcamera {
|
|
|
|
LOG_DECLARE_CATEGORY(Awb)
|
|
|
|
namespace {
|
|
|
|
template<typename T>
|
|
class LimitsRecorder
|
|
{
|
|
public:
|
|
LimitsRecorder()
|
|
: min_(std::numeric_limits<T>::max()),
|
|
max_(std::numeric_limits<T>::min())
|
|
{
|
|
}
|
|
|
|
void record(const T &value)
|
|
{
|
|
min_ = std::min(min_, value);
|
|
max_ = std::max(max_, value);
|
|
}
|
|
|
|
const T &min() const { return min_; }
|
|
const T &max() const { return max_; }
|
|
|
|
private:
|
|
T min_;
|
|
T max_;
|
|
};
|
|
|
|
#ifndef __DOXYGEN__
|
|
template<typename T>
|
|
std::ostream &operator<<(std::ostream &out, const LimitsRecorder<T> &v)
|
|
{
|
|
out << "[ " << v.min() << ", " << v.max() << " ]";
|
|
return out;
|
|
}
|
|
#endif
|
|
|
|
} /* namespace */
|
|
|
|
namespace ipa {
|
|
|
|
/**
|
|
* \brief Step size control for CT search
|
|
*/
|
|
constexpr double kSearchStep = 0.2;
|
|
|
|
/**
|
|
* \copydoc libcamera::ipa::Interpolator::interpolate()
|
|
*/
|
|
template<>
|
|
void Interpolator<Pwl>::interpolate(const Pwl &a, const Pwl &b, Pwl &dest, double lambda)
|
|
{
|
|
dest = Pwl::combine(a, b,
|
|
[=](double /*x*/, double y0, double y1) -> double {
|
|
return y0 * (1.0 - lambda) + y1 * lambda;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* \class AwbBayes
|
|
* \brief Implementation of a bayesian auto white balance algorithm
|
|
*
|
|
* In a bayesian AWB algorithm the auto white balance estimation is improved by
|
|
* taking the likelihood of a given lightsource based on the estimated lux level
|
|
* into account. E.g. If it is very bright we can assume that we are outside and
|
|
* that colour temperatures around 6500 are preferred.
|
|
*
|
|
* The second part of this algorithm is the search for the most likely colour
|
|
* temperature. It is implemented in AwbBayes::coarseSearch() and in
|
|
* AwbBayes::fineSearch(). The search works very well without prior likelihoods
|
|
* and therefore the algorithm itself provides very good results even without
|
|
* prior likelihoods.
|
|
*/
|
|
|
|
/**
|
|
* \var AwbBayes::transversePos_
|
|
* \brief How far to wander off CT curve towards "more purple"
|
|
*/
|
|
|
|
/**
|
|
* \var AwbBayes::transverseNeg_
|
|
* \brief How far to wander off CT curve towards "more green"
|
|
*/
|
|
|
|
/**
|
|
* \var AwbBayes::currentMode_
|
|
* \brief The currently selected mode
|
|
*/
|
|
|
|
int AwbBayes::init(const YamlObject &tuningData)
|
|
{
|
|
int ret = colourGainCurve_.readYaml(tuningData["colourGains"], "ct", "gains");
|
|
if (ret) {
|
|
LOG(Awb, Error)
|
|
<< "Failed to parse 'colourGains' "
|
|
<< "parameter from tuning file";
|
|
return ret;
|
|
}
|
|
|
|
ctR_.clear();
|
|
ctB_.clear();
|
|
for (const auto &[ct, g] : colourGainCurve_.data()) {
|
|
ctR_.append(ct, 1.0 / g[0]);
|
|
ctB_.append(ct, 1.0 / g[1]);
|
|
}
|
|
|
|
/* We will want the inverse functions of these too. */
|
|
ctRInverse_ = ctR_.inverse().first;
|
|
ctBInverse_ = ctB_.inverse().first;
|
|
|
|
ret = readPriors(tuningData);
|
|
if (ret) {
|
|
LOG(Awb, Error) << "Failed to read priors";
|
|
return ret;
|
|
}
|
|
|
|
ret = parseModeConfigs(tuningData, controls::AwbAuto);
|
|
if (ret) {
|
|
LOG(Awb, Error)
|
|
<< "Failed to parse mode parameter from tuning file";
|
|
return ret;
|
|
}
|
|
currentMode_ = &modes_[controls::AwbAuto];
|
|
|
|
transversePos_ = tuningData["transversePos"].get<double>(0.01);
|
|
transverseNeg_ = tuningData["transverseNeg"].get<double>(0.01);
|
|
if (transversePos_ <= 0 || transverseNeg_ <= 0) {
|
|
LOG(Awb, Error) << "AwbConfig: transversePos/Neg must be > 0";
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int AwbBayes::readPriors(const YamlObject &tuningData)
|
|
{
|
|
const auto &priorsList = tuningData["priors"];
|
|
std::map<uint32_t, Pwl> priors;
|
|
|
|
if (!priorsList) {
|
|
LOG(Awb, Error) << "No priors specified";
|
|
return -EINVAL;
|
|
}
|
|
|
|
for (const auto &p : priorsList.asList()) {
|
|
if (!p.contains("lux")) {
|
|
LOG(Awb, Error) << "Missing lux value";
|
|
return -EINVAL;
|
|
}
|
|
|
|
uint32_t lux = p["lux"].get<uint32_t>(0);
|
|
if (priors.count(lux)) {
|
|
LOG(Awb, Error) << "Duplicate prior for lux value " << lux;
|
|
return -EINVAL;
|
|
}
|
|
|
|
std::vector<uint32_t> temperatures =
|
|
p["ct"].getList<uint32_t>().value_or(std::vector<uint32_t>{});
|
|
std::vector<double> probabilities =
|
|
p["probability"].getList<double>().value_or(std::vector<double>{});
|
|
|
|
if (temperatures.size() != probabilities.size()) {
|
|
LOG(Awb, Error)
|
|
<< "Ct and probability array sizes are unequal";
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (temperatures.empty()) {
|
|
LOG(Awb, Error)
|
|
<< "Ct and probability arrays are empty";
|
|
return -EINVAL;
|
|
}
|
|
|
|
std::map<int, double> ctToProbability;
|
|
for (unsigned int i = 0; i < temperatures.size(); i++) {
|
|
int t = temperatures[i];
|
|
if (ctToProbability.count(t)) {
|
|
LOG(Awb, Error) << "Duplicate ct value";
|
|
return -EINVAL;
|
|
}
|
|
|
|
ctToProbability[t] = probabilities[i];
|
|
}
|
|
|
|
auto &pwl = priors[lux];
|
|
for (const auto &[ct, prob] : ctToProbability) {
|
|
if (prob < 1e-6) {
|
|
LOG(Awb, Error) << "Prior probability must be larger than 1e-6";
|
|
return -EINVAL;
|
|
}
|
|
pwl.append(ct, prob);
|
|
}
|
|
}
|
|
|
|
if (priors.empty()) {
|
|
LOG(Awb, Error) << "No priors specified";
|
|
return -EINVAL;
|
|
}
|
|
|
|
priors_.setData(std::move(priors));
|
|
|
|
return 0;
|
|
}
|
|
|
|
void AwbBayes::handleControls(const ControlList &controls)
|
|
{
|
|
auto mode = controls.get(controls::AwbMode);
|
|
if (mode) {
|
|
auto it = modes_.find(static_cast<controls::AwbModeEnum>(*mode));
|
|
if (it != modes_.end())
|
|
currentMode_ = &it->second;
|
|
else
|
|
LOG(Awb, Error) << "Unsupported AWB mode " << *mode;
|
|
}
|
|
}
|
|
|
|
RGB<double> AwbBayes::gainsFromColourTemperature(double colourTemperature)
|
|
{
|
|
/*
|
|
* \todo: In the RaspberryPi code, the ct curve was interpolated in
|
|
* the white point space (1/x) not in gains space. This feels counter
|
|
* intuitive, as the gains are in linear space. But I can't prove it.
|
|
*/
|
|
const auto &gains = colourGainCurve_.getInterpolated(colourTemperature);
|
|
return { { gains[0], 1.0, gains[1] } };
|
|
}
|
|
|
|
AwbResult AwbBayes::calculateAwb(const AwbStats &stats, unsigned int lux)
|
|
{
|
|
ipa::Pwl prior;
|
|
if (lux > 0) {
|
|
prior = priors_.getInterpolated(lux);
|
|
prior.map([](double x, double y) {
|
|
LOG(Awb, Debug) << "(" << x << "," << y << ")";
|
|
});
|
|
} else {
|
|
prior.append(0, 1.0);
|
|
}
|
|
|
|
double t = coarseSearch(prior, stats);
|
|
double r = ctR_.eval(t);
|
|
double b = ctB_.eval(t);
|
|
LOG(Awb, Debug)
|
|
<< "After coarse search: r " << r << " b " << b << " (gains r "
|
|
<< 1 / r << " b " << 1 / b << ")";
|
|
|
|
/*
|
|
* Original comment from RaspberryPi:
|
|
* Not entirely sure how to handle the fine search yet. Mostly the
|
|
* estimated CT is already good enough, but the fine search allows us to
|
|
* wander transversely off the CT curve. Under some illuminants, where
|
|
* there may be more or less green light, this may prove beneficial,
|
|
* though I probably need more real datasets before deciding exactly how
|
|
* this should be controlled and tuned.
|
|
*/
|
|
fineSearch(t, r, b, prior, stats);
|
|
LOG(Awb, Debug)
|
|
<< "After fine search: r " << r << " b " << b << " (gains r "
|
|
<< 1 / r << " b " << 1 / b << ")";
|
|
|
|
return { { { 1.0 / r, 1.0, 1.0 / b } }, t };
|
|
}
|
|
|
|
double AwbBayes::coarseSearch(const ipa::Pwl &prior, const AwbStats &stats) const
|
|
{
|
|
std::vector<Pwl::Point> points;
|
|
size_t bestPoint = 0;
|
|
double t = currentMode_->ctLo;
|
|
int spanR = -1;
|
|
int spanB = -1;
|
|
LimitsRecorder<double> errorLimits;
|
|
LimitsRecorder<double> priorLogLikelihoodLimits;
|
|
|
|
/* Step down the CT curve evaluating log likelihood. */
|
|
while (true) {
|
|
double r = ctR_.eval(t, &spanR);
|
|
double b = ctB_.eval(t, &spanB);
|
|
RGB<double> gains({ 1 / r, 1.0, 1 / b });
|
|
double delta2Sum = stats.computeColourError(gains);
|
|
double priorLogLikelihood = log(prior.eval(prior.domain().clamp(t)));
|
|
double finalLogLikelihood = delta2Sum - priorLogLikelihood;
|
|
|
|
errorLimits.record(delta2Sum);
|
|
priorLogLikelihoodLimits.record(priorLogLikelihood);
|
|
|
|
points.push_back({ { t, finalLogLikelihood } });
|
|
if (points.back().y() < points[bestPoint].y())
|
|
bestPoint = points.size() - 1;
|
|
|
|
if (t == currentMode_->ctHi)
|
|
break;
|
|
|
|
/*
|
|
* Ensure even steps along the r/b curve by scaling them by the
|
|
* current t.
|
|
*/
|
|
t = std::min(t + t / 10 * kSearchStep, currentMode_->ctHi);
|
|
}
|
|
|
|
t = points[bestPoint].x();
|
|
LOG(Awb, Debug) << "Coarse search found CT " << t
|
|
<< " error limits:" << errorLimits
|
|
<< " prior log likelihood limits:" << priorLogLikelihoodLimits;
|
|
|
|
/*
|
|
* We have the best point of the search, but refine it with a quadratic
|
|
* interpolation around its neighbors.
|
|
*/
|
|
if (points.size() > 2) {
|
|
bestPoint = std::clamp(bestPoint, std::size_t{ 1 }, points.size() - 2);
|
|
t = interpolateQuadratic(points[bestPoint - 1],
|
|
points[bestPoint],
|
|
points[bestPoint + 1]);
|
|
LOG(Awb, Debug)
|
|
<< "After quadratic refinement, coarse search has CT "
|
|
<< t;
|
|
}
|
|
|
|
return t;
|
|
}
|
|
|
|
void AwbBayes::fineSearch(double &t, double &r, double &b, ipa::Pwl const &prior, const AwbStats &stats) const
|
|
{
|
|
int spanR = -1;
|
|
int spanB = -1;
|
|
double step = t / 10 * kSearchStep * 0.1;
|
|
int nsteps = 5;
|
|
ctR_.eval(t, &spanR);
|
|
ctB_.eval(t, &spanB);
|
|
double rDiff = ctR_.eval(t + nsteps * step, &spanR) -
|
|
ctR_.eval(t - nsteps * step, &spanR);
|
|
double bDiff = ctB_.eval(t + nsteps * step, &spanB) -
|
|
ctB_.eval(t - nsteps * step, &spanB);
|
|
Pwl::Point transverse({ bDiff, -rDiff });
|
|
if (transverse.length2() < 1e-6)
|
|
return;
|
|
/*
|
|
* transverse is a unit vector orthogonal to the b vs. r function
|
|
* (pointing outwards with r and b increasing)
|
|
*/
|
|
transverse = transverse / transverse.length();
|
|
double bestLogLikelihood = 0;
|
|
double bestT = 0;
|
|
Pwl::Point bestRB(0);
|
|
double transverseRange = transverseNeg_ + transversePos_;
|
|
const int maxNumDeltas = 12;
|
|
LimitsRecorder<double> errorLimits;
|
|
LimitsRecorder<double> priorLogLikelihoodLimits;
|
|
|
|
|
|
/* a transverse step approximately every 0.01 r/b units */
|
|
int numDeltas = floor(transverseRange * 100 + 0.5) + 1;
|
|
numDeltas = std::clamp(numDeltas, 3, maxNumDeltas);
|
|
|
|
/*
|
|
* Step down CT curve. March a bit further if the transverse range is
|
|
* large.
|
|
*/
|
|
nsteps += numDeltas;
|
|
for (int i = -nsteps; i <= nsteps; i++) {
|
|
double tTest = t + i * step;
|
|
double priorLogLikelihood =
|
|
log(prior.eval(prior.domain().clamp(tTest)));
|
|
priorLogLikelihoodLimits.record(priorLogLikelihood);
|
|
Pwl::Point rbStart{ { ctR_.eval(tTest, &spanR),
|
|
ctB_.eval(tTest, &spanB) } };
|
|
Pwl::Point samples[maxNumDeltas];
|
|
int bestPoint = 0;
|
|
|
|
/*
|
|
* Sample numDeltas points transversely *off* the CT curve
|
|
* in the range [-transverseNeg, transversePos].
|
|
* The x values of a sample contains the distance and the y
|
|
* value contains the corresponding log likelihood.
|
|
*/
|
|
double transverseStep = transverseRange / (numDeltas - 1);
|
|
for (int j = 0; j < numDeltas; j++) {
|
|
auto &p = samples[j];
|
|
p.x() = -transverseNeg_ + transverseStep * j;
|
|
Pwl::Point rbTest = rbStart + transverse * p.x();
|
|
RGB<double> gains({ 1 / rbTest[0], 1.0, 1 / rbTest[1] });
|
|
double delta2Sum = stats.computeColourError(gains);
|
|
errorLimits.record(delta2Sum);
|
|
p.y() = delta2Sum - priorLogLikelihood;
|
|
|
|
if (p.y() < samples[bestPoint].y())
|
|
bestPoint = j;
|
|
}
|
|
|
|
/*
|
|
* We have all samples transversely across the CT curve,
|
|
* now let's do a quadratic interpolation for the best result.
|
|
*/
|
|
bestPoint = std::clamp(bestPoint, 1, numDeltas - 2);
|
|
double bestOffset = interpolateQuadratic(samples[bestPoint - 1],
|
|
samples[bestPoint],
|
|
samples[bestPoint + 1]);
|
|
Pwl::Point rbTest = rbStart + transverse * bestOffset;
|
|
RGB<double> gains({ 1 / rbTest[0], 1.0, 1 / rbTest[1] });
|
|
double delta2Sum = stats.computeColourError(gains);
|
|
errorLimits.record(delta2Sum);
|
|
double finalLogLikelihood = delta2Sum - priorLogLikelihood;
|
|
|
|
if (bestT == 0 || finalLogLikelihood < bestLogLikelihood) {
|
|
bestLogLikelihood = finalLogLikelihood;
|
|
bestT = tTest;
|
|
bestRB = rbTest;
|
|
}
|
|
}
|
|
|
|
t = bestT;
|
|
r = bestRB[0];
|
|
b = bestRB[1];
|
|
LOG(Awb, Debug)
|
|
<< "Fine search found t " << t << " r " << r << " b " << b
|
|
<< " error limits: " << errorLimits
|
|
<< " prior log likelihood limits: " << priorLogLikelihoodLimits;
|
|
}
|
|
|
|
/**
|
|
* \brief Find extremum of function
|
|
* \param[in] a First point
|
|
* \param[in] b Second point
|
|
* \param[in] c Third point
|
|
*
|
|
* Given 3 points on a curve, find the extremum of the function in that interval
|
|
* by fitting a quadratic.
|
|
*
|
|
* \return The x value of the extremum clamped to the interval [a.x(), c.x()]
|
|
*/
|
|
double AwbBayes::interpolateQuadratic(Pwl::Point const &a, Pwl::Point const &b,
|
|
Pwl::Point const &c) const
|
|
{
|
|
const double eps = 1e-3;
|
|
Pwl::Point ca = c - a;
|
|
Pwl::Point ba = b - a;
|
|
double denominator = 2 * (ba.y() * ca.x() - ca.y() * ba.x());
|
|
if (std::abs(denominator) > eps) {
|
|
double numerator = ba.y() * ca.x() * ca.x() - ca.y() * ba.x() * ba.x();
|
|
double result = numerator / denominator + a.x();
|
|
return std::max(a.x(), std::min(c.x(), result));
|
|
}
|
|
/* has degenerated to straight line segment */
|
|
return a.y() < c.y() - eps ? a.x() : (c.y() < a.y() - eps ? c.x() : b.x());
|
|
}
|
|
|
|
} /* namespace ipa */
|
|
|
|
} /* namespace libcamera */
|