libcamera/src/ipa/raspberrypi/controller/rpi/agc.cpp
Naushir Patuck f95bae418c raspberrypi: Update Copyright statement in all Raspberry Pi source files
s/Raspberry Pi (Trading) Limited/Raspberry Pi Ltd/ to reflect the new
Raspberry Pi entity name.

Signed-off-by: Naushir Patuck <naush@raspberrypi.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
2022-07-27 18:12:15 +03:00

826 lines
28 KiB
C++

/* SPDX-License-Identifier: BSD-2-Clause */
/*
* Copyright (C) 2019, Raspberry Pi Ltd
*
* agc.cpp - AGC/AEC control algorithm
*/
#include <map>
#include <linux/bcm2835-isp.h>
#include <libcamera/base/log.h>
#include "../awb_status.h"
#include "../device_status.h"
#include "../histogram.h"
#include "../lux_status.h"
#include "../metadata.h"
#include "agc.h"
using namespace RPiController;
using namespace libcamera;
using libcamera::utils::Duration;
using namespace std::literals::chrono_literals;
LOG_DEFINE_CATEGORY(RPiAgc)
#define NAME "rpi.agc"
#define PIPELINE_BITS 13 /* seems to be a 13-bit pipeline */
void AgcMeteringMode::read(boost::property_tree::ptree const &params)
{
int num = 0;
for (auto &p : params.get_child("weights")) {
if (num == AGC_STATS_SIZE)
throw std::runtime_error("AgcConfig: too many weights");
weights[num++] = p.second.get_value<double>();
}
if (num != AGC_STATS_SIZE)
throw std::runtime_error("AgcConfig: insufficient weights");
}
static std::string
readMeteringModes(std::map<std::string, AgcMeteringMode> &meteringModes,
boost::property_tree::ptree const &params)
{
std::string first;
for (auto &p : params) {
AgcMeteringMode meteringMode;
meteringMode.read(p.second);
meteringModes[p.first] = std::move(meteringMode);
if (first.empty())
first = p.first;
}
return first;
}
static int readList(std::vector<double> &list,
boost::property_tree::ptree const &params)
{
for (auto &p : params)
list.push_back(p.second.get_value<double>());
return list.size();
}
static int readList(std::vector<Duration> &list,
boost::property_tree::ptree const &params)
{
for (auto &p : params)
list.push_back(p.second.get_value<double>() * 1us);
return list.size();
}
void AgcExposureMode::read(boost::property_tree::ptree const &params)
{
int numShutters = readList(shutter, params.get_child("shutter"));
int numAgs = readList(gain, params.get_child("gain"));
if (numShutters < 2 || numAgs < 2)
throw std::runtime_error(
"AgcConfig: must have at least two entries in exposure profile");
if (numShutters != numAgs)
throw std::runtime_error(
"AgcConfig: expect same number of exposure and gain entries in exposure profile");
}
static std::string
readExposureModes(std::map<std::string, AgcExposureMode> &exposureModes,
boost::property_tree::ptree const &params)
{
std::string first;
for (auto &p : params) {
AgcExposureMode exposureMode;
exposureMode.read(p.second);
exposureModes[p.first] = std::move(exposureMode);
if (first.empty())
first = p.first;
}
return first;
}
void AgcConstraint::read(boost::property_tree::ptree const &params)
{
std::string boundString = params.get<std::string>("bound", "");
transform(boundString.begin(), boundString.end(),
boundString.begin(), ::toupper);
if (boundString != "UPPER" && boundString != "LOWER")
throw std::runtime_error(
"AGC constraint type should be UPPER or LOWER");
bound = boundString == "UPPER" ? Bound::UPPER : Bound::LOWER;
qLo = params.get<double>("q_lo");
qHi = params.get<double>("q_hi");
yTarget.read(params.get_child("y_target"));
}
static AgcConstraintMode
readConstraintMode(boost::property_tree::ptree const &params)
{
AgcConstraintMode mode;
for (auto &p : params) {
AgcConstraint constraint;
constraint.read(p.second);
mode.push_back(std::move(constraint));
}
return mode;
}
static std::string readConstraintModes(std::map<std::string, AgcConstraintMode> &constraintModes,
boost::property_tree::ptree const &params)
{
std::string first;
for (auto &p : params) {
constraintModes[p.first] = readConstraintMode(p.second);
if (first.empty())
first = p.first;
}
return first;
}
void AgcConfig::read(boost::property_tree::ptree const &params)
{
LOG(RPiAgc, Debug) << "AgcConfig";
defaultMeteringMode = readMeteringModes(meteringModes, params.get_child("metering_modes"));
defaultExposureMode = readExposureModes(exposureModes, params.get_child("exposure_modes"));
defaultConstraintMode = readConstraintModes(constraintModes, params.get_child("constraint_modes"));
yTarget.read(params.get_child("y_target"));
speed = params.get<double>("speed", 0.2);
startupFrames = params.get<uint16_t>("startup_frames", 10);
convergenceFrames = params.get<unsigned int>("convergence_frames", 6);
fastReduceThreshold = params.get<double>("fast_reduce_threshold", 0.4);
baseEv = params.get<double>("base_ev", 1.0);
/* Start with quite a low value as ramping up is easier than ramping down. */
defaultExposureTime = params.get<double>("default_exposure_time", 1000) * 1us;
defaultAnalogueGain = params.get<double>("default_analogueGain", 1.0);
}
Agc::ExposureValues::ExposureValues()
: shutter(0s), analogueGain(0),
totalExposure(0s), totalExposureNoDG(0s)
{
}
Agc::Agc(Controller *controller)
: AgcAlgorithm(controller), meteringMode_(nullptr),
exposureMode_(nullptr), constraintMode_(nullptr),
frameCount_(0), lockCount_(0),
lastTargetExposure_(0s), lastSensitivity_(0.0),
ev_(1.0), flickerPeriod_(0s),
maxShutter_(0s), fixedShutter_(0s), fixedAnalogueGain_(0.0)
{
memset(&awb_, 0, sizeof(awb_));
/*
* Setting status_.totalExposureValue_ to zero initially tells us
* it's not been calculated yet (i.e. Process hasn't yet run).
*/
memset(&status_, 0, sizeof(status_));
status_.ev = ev_;
}
char const *Agc::name() const
{
return NAME;
}
void Agc::read(boost::property_tree::ptree const &params)
{
LOG(RPiAgc, Debug) << "Agc";
config_.read(params);
/*
* Set the config's defaults (which are the first ones it read) as our
* current modes, until someone changes them. (they're all known to
* exist at this point)
*/
meteringModeName_ = config_.defaultMeteringMode;
meteringMode_ = &config_.meteringModes[meteringModeName_];
exposureModeName_ = config_.defaultExposureMode;
exposureMode_ = &config_.exposureModes[exposureModeName_];
constraintModeName_ = config_.defaultConstraintMode;
constraintMode_ = &config_.constraintModes[constraintModeName_];
/* Set up the "last shutter/gain" values, in case AGC starts "disabled". */
status_.shutterTime = config_.defaultExposureTime;
status_.analogueGain = config_.defaultAnalogueGain;
}
bool Agc::isPaused() const
{
return false;
}
void Agc::pause()
{
fixedShutter_ = status_.shutterTime;
fixedAnalogueGain_ = status_.analogueGain;
}
void Agc::resume()
{
fixedShutter_ = 0s;
fixedAnalogueGain_ = 0;
}
unsigned int Agc::getConvergenceFrames() const
{
/*
* If shutter and gain have been explicitly set, there is no
* convergence to happen, so no need to drop any frames - return zero.
*/
if (fixedShutter_ && fixedAnalogueGain_)
return 0;
else
return config_.convergenceFrames;
}
void Agc::setEv(double ev)
{
ev_ = ev;
}
void Agc::setFlickerPeriod(Duration flickerPeriod)
{
flickerPeriod_ = flickerPeriod;
}
void Agc::setMaxShutter(Duration maxShutter)
{
maxShutter_ = maxShutter;
}
void Agc::setFixedShutter(Duration fixedShutter)
{
fixedShutter_ = fixedShutter;
/* Set this in case someone calls Pause() straight after. */
status_.shutterTime = clipShutter(fixedShutter_);
}
void Agc::setFixedAnalogueGain(double fixedAnalogueGain)
{
fixedAnalogueGain_ = fixedAnalogueGain;
/* Set this in case someone calls Pause() straight after. */
status_.analogueGain = fixedAnalogueGain;
}
void Agc::setMeteringMode(std::string const &meteringModeName)
{
meteringModeName_ = meteringModeName;
}
void Agc::setExposureMode(std::string const &exposureModeName)
{
exposureModeName_ = exposureModeName;
}
void Agc::setConstraintMode(std::string const &constraintModeName)
{
constraintModeName_ = constraintModeName;
}
void Agc::switchMode(CameraMode const &cameraMode,
Metadata *metadata)
{
/* AGC expects the mode sensitivity always to be non-zero. */
ASSERT(cameraMode.sensitivity);
housekeepConfig();
Duration fixedShutter = clipShutter(fixedShutter_);
if (fixedShutter && fixedAnalogueGain_) {
/* We're going to reset the algorithm here with these fixed values. */
fetchAwbStatus(metadata);
double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });
ASSERT(minColourGain != 0.0);
/* This is the equivalent of computeTargetExposure and applyDigitalGain. */
target_.totalExposureNoDG = fixedShutter_ * fixedAnalogueGain_;
target_.totalExposure = target_.totalExposureNoDG / minColourGain;
/* Equivalent of filterExposure. This resets any "history". */
filtered_ = target_;
/* Equivalent of divideUpExposure. */
filtered_.shutter = fixedShutter;
filtered_.analogueGain = fixedAnalogueGain_;
} else if (status_.totalExposureValue) {
/*
* On a mode switch, various things could happen:
* - the exposure profile might change
* - a fixed exposure or gain might be set
* - the new mode's sensitivity might be different
* We cope with the last of these by scaling the target values. After
* that we just need to re-divide the exposure/gain according to the
* current exposure profile, which takes care of everything else.
*/
double ratio = lastSensitivity_ / cameraMode.sensitivity;
target_.totalExposureNoDG *= ratio;
target_.totalExposure *= ratio;
filtered_.totalExposureNoDG *= ratio;
filtered_.totalExposure *= ratio;
divideUpExposure();
} else {
/*
* We come through here on startup, when at least one of the shutter
* or gain has not been fixed. We must still write those values out so
* that they will be applied immediately. We supply some arbitrary defaults
* for any that weren't set.
*/
/* Equivalent of divideUpExposure. */
filtered_.shutter = fixedShutter ? fixedShutter : config_.defaultExposureTime;
filtered_.analogueGain = fixedAnalogueGain_ ? fixedAnalogueGain_ : config_.defaultAnalogueGain;
}
writeAndFinish(metadata, false);
/* We must remember the sensitivity of this mode for the next SwitchMode. */
lastSensitivity_ = cameraMode.sensitivity;
}
void Agc::prepare(Metadata *imageMetadata)
{
status_.digitalGain = 1.0;
fetchAwbStatus(imageMetadata); /* always fetch it so that Process knows it's been done */
if (status_.totalExposureValue) {
/* Process has run, so we have meaningful values. */
DeviceStatus deviceStatus;
if (imageMetadata->get("device.status", deviceStatus) == 0) {
Duration actualExposure = deviceStatus.shutterSpeed *
deviceStatus.analogueGain;
if (actualExposure) {
status_.digitalGain = status_.totalExposureValue / actualExposure;
LOG(RPiAgc, Debug) << "Want total exposure " << status_.totalExposureValue;
/*
* Never ask for a gain < 1.0, and also impose
* some upper limit. Make it customisable?
*/
status_.digitalGain = std::max(1.0, std::min(status_.digitalGain, 4.0));
LOG(RPiAgc, Debug) << "Actual exposure " << actualExposure;
LOG(RPiAgc, Debug) << "Use digitalGain " << status_.digitalGain;
LOG(RPiAgc, Debug) << "Effective exposure "
<< actualExposure * status_.digitalGain;
/* Decide whether AEC/AGC has converged. */
updateLockStatus(deviceStatus);
}
} else
LOG(RPiAgc, Warning) << name() << ": no device metadata";
imageMetadata->set("agc.status", status_);
}
}
void Agc::process(StatisticsPtr &stats, Metadata *imageMetadata)
{
frameCount_++;
/*
* First a little bit of housekeeping, fetching up-to-date settings and
* configuration, that kind of thing.
*/
housekeepConfig();
/* Get the current exposure values for the frame that's just arrived. */
fetchCurrentExposure(imageMetadata);
/* Compute the total gain we require relative to the current exposure. */
double gain, targetY;
computeGain(stats.get(), imageMetadata, gain, targetY);
/* Now compute the target (final) exposure which we think we want. */
computeTargetExposure(gain);
/*
* Some of the exposure has to be applied as digital gain, so work out
* what that is. This function also tells us whether it's decided to
* "desaturate" the image more quickly.
*/
bool desaturate = applyDigitalGain(gain, targetY);
/* The results have to be filtered so as not to change too rapidly. */
filterExposure(desaturate);
/*
* The last thing is to divide up the exposure value into a shutter time
* and analogue gain, according to the current exposure mode.
*/
divideUpExposure();
/* Finally advertise what we've done. */
writeAndFinish(imageMetadata, desaturate);
}
void Agc::updateLockStatus(DeviceStatus const &deviceStatus)
{
const double errorFactor = 0.10; /* make these customisable? */
const int maxLockCount = 5;
/* Reset "lock count" when we exceed this multiple of errorFactor */
const double resetMargin = 1.5;
/* Add 200us to the exposure time error to allow for line quantisation. */
Duration exposureError = lastDeviceStatus_.shutterSpeed * errorFactor + 200us;
double gainError = lastDeviceStatus_.analogueGain * errorFactor;
Duration targetError = lastTargetExposure_ * errorFactor;
/*
* Note that we don't know the exposure/gain limits of the sensor, so
* the values we keep requesting may be unachievable. For this reason
* we only insist that we're close to values in the past few frames.
*/
if (deviceStatus.shutterSpeed > lastDeviceStatus_.shutterSpeed - exposureError &&
deviceStatus.shutterSpeed < lastDeviceStatus_.shutterSpeed + exposureError &&
deviceStatus.analogueGain > lastDeviceStatus_.analogueGain - gainError &&
deviceStatus.analogueGain < lastDeviceStatus_.analogueGain + gainError &&
status_.targetExposureValue > lastTargetExposure_ - targetError &&
status_.targetExposureValue < lastTargetExposure_ + targetError)
lockCount_ = std::min(lockCount_ + 1, maxLockCount);
else if (deviceStatus.shutterSpeed < lastDeviceStatus_.shutterSpeed - resetMargin * exposureError ||
deviceStatus.shutterSpeed > lastDeviceStatus_.shutterSpeed + resetMargin * exposureError ||
deviceStatus.analogueGain < lastDeviceStatus_.analogueGain - resetMargin * gainError ||
deviceStatus.analogueGain > lastDeviceStatus_.analogueGain + resetMargin * gainError ||
status_.targetExposureValue < lastTargetExposure_ - resetMargin * targetError ||
status_.targetExposureValue > lastTargetExposure_ + resetMargin * targetError)
lockCount_ = 0;
lastDeviceStatus_ = deviceStatus;
lastTargetExposure_ = status_.targetExposureValue;
LOG(RPiAgc, Debug) << "Lock count updated to " << lockCount_;
status_.locked = lockCount_ == maxLockCount;
}
static void copyString(std::string const &s, char *d, size_t size)
{
size_t length = s.copy(d, size - 1);
d[length] = '\0';
}
void Agc::housekeepConfig()
{
/* First fetch all the up-to-date settings, so no one else has to do it. */
status_.ev = ev_;
status_.fixedShutter = clipShutter(fixedShutter_);
status_.fixedAnalogueGain = fixedAnalogueGain_;
status_.flickerPeriod = flickerPeriod_;
LOG(RPiAgc, Debug) << "ev " << status_.ev << " fixedShutter "
<< status_.fixedShutter << " fixedAnalogueGain "
<< status_.fixedAnalogueGain;
/*
* Make sure the "mode" pointers point to the up-to-date things, if
* they've changed.
*/
if (strcmp(meteringModeName_.c_str(), status_.meteringMode)) {
auto it = config_.meteringModes.find(meteringModeName_);
if (it == config_.meteringModes.end())
throw std::runtime_error("Agc: no metering mode " +
meteringModeName_);
meteringMode_ = &it->second;
copyString(meteringModeName_, status_.meteringMode,
sizeof(status_.meteringMode));
}
if (strcmp(exposureModeName_.c_str(), status_.exposureMode)) {
auto it = config_.exposureModes.find(exposureModeName_);
if (it == config_.exposureModes.end())
throw std::runtime_error("Agc: no exposure profile " +
exposureModeName_);
exposureMode_ = &it->second;
copyString(exposureModeName_, status_.exposureMode,
sizeof(status_.exposureMode));
}
if (strcmp(constraintModeName_.c_str(), status_.constraintMode)) {
auto it =
config_.constraintModes.find(constraintModeName_);
if (it == config_.constraintModes.end())
throw std::runtime_error("Agc: no constraint list " +
constraintModeName_);
constraintMode_ = &it->second;
copyString(constraintModeName_, status_.constraintMode,
sizeof(status_.constraintMode));
}
LOG(RPiAgc, Debug) << "exposureMode "
<< exposureModeName_ << " constraintMode "
<< constraintModeName_ << " meteringMode "
<< meteringModeName_;
}
void Agc::fetchCurrentExposure(Metadata *imageMetadata)
{
std::unique_lock<Metadata> lock(*imageMetadata);
DeviceStatus *deviceStatus =
imageMetadata->getLocked<DeviceStatus>("device.status");
if (!deviceStatus)
throw std::runtime_error("Agc: no device metadata");
current_.shutter = deviceStatus->shutterSpeed;
current_.analogueGain = deviceStatus->analogueGain;
AgcStatus *agcStatus =
imageMetadata->getLocked<AgcStatus>("agc.status");
current_.totalExposure = agcStatus ? agcStatus->totalExposureValue : 0s;
current_.totalExposureNoDG = current_.shutter * current_.analogueGain;
}
void Agc::fetchAwbStatus(Metadata *imageMetadata)
{
awb_.gainR = 1.0; /* in case not found in metadata */
awb_.gainG = 1.0;
awb_.gainB = 1.0;
if (imageMetadata->get("awb.status", awb_) != 0)
LOG(RPiAgc, Debug) << "Agc: no AWB status found";
}
static double computeInitialY(bcm2835_isp_stats *stats, AwbStatus const &awb,
double weights[], double gain)
{
bcm2835_isp_stats_region *regions = stats->agc_stats;
/*
* Note how the calculation below means that equal weights give you
* "average" metering (i.e. all pixels equally important).
*/
double rSum = 0, gSum = 0, bSum = 0, pixelSum = 0;
for (int i = 0; i < AGC_STATS_SIZE; i++) {
double counted = regions[i].counted;
double rAcc = std::min(regions[i].r_sum * gain, ((1 << PIPELINE_BITS) - 1) * counted);
double gAcc = std::min(regions[i].g_sum * gain, ((1 << PIPELINE_BITS) - 1) * counted);
double bAcc = std::min(regions[i].b_sum * gain, ((1 << PIPELINE_BITS) - 1) * counted);
rSum += rAcc * weights[i];
gSum += gAcc * weights[i];
bSum += bAcc * weights[i];
pixelSum += counted * weights[i];
}
if (pixelSum == 0.0) {
LOG(RPiAgc, Warning) << "computeInitialY: pixelSum is zero";
return 0;
}
double ySum = rSum * awb.gainR * .299 +
gSum * awb.gainG * .587 +
bSum * awb.gainB * .114;
return ySum / pixelSum / (1 << PIPELINE_BITS);
}
/*
* We handle extra gain through EV by adjusting our Y targets. However, you
* simply can't monitor histograms once they get very close to (or beyond!)
* saturation, so we clamp the Y targets to this value. It does mean that EV
* increases don't necessarily do quite what you might expect in certain
* (contrived) cases.
*/
#define EV_GAIN_Y_TARGET_LIMIT 0.9
static double constraintComputeGain(AgcConstraint &c, Histogram &h, double lux,
double evGain, double &targetY)
{
targetY = c.yTarget.eval(c.yTarget.domain().clip(lux));
targetY = std::min(EV_GAIN_Y_TARGET_LIMIT, targetY * evGain);
double iqm = h.interQuantileMean(c.qLo, c.qHi);
return (targetY * NUM_HISTOGRAM_BINS) / iqm;
}
void Agc::computeGain(bcm2835_isp_stats *statistics, Metadata *imageMetadata,
double &gain, double &targetY)
{
struct LuxStatus lux = {};
lux.lux = 400; /* default lux level to 400 in case no metadata found */
if (imageMetadata->get("lux.status", lux) != 0)
LOG(RPiAgc, Warning) << "Agc: no lux level found";
Histogram h(statistics->hist[0].g_hist, NUM_HISTOGRAM_BINS);
double evGain = status_.ev * config_.baseEv;
/*
* The initial gain and target_Y come from some of the regions. After
* that we consider the histogram constraints.
*/
targetY = config_.yTarget.eval(config_.yTarget.domain().clip(lux.lux));
targetY = std::min(EV_GAIN_Y_TARGET_LIMIT, targetY * evGain);
/*
* Do this calculation a few times as brightness increase can be
* non-linear when there are saturated regions.
*/
gain = 1.0;
for (int i = 0; i < 8; i++) {
double initialY = computeInitialY(statistics, awb_, meteringMode_->weights, gain);
double extraGain = std::min(10.0, targetY / (initialY + .001));
gain *= extraGain;
LOG(RPiAgc, Debug) << "Initial Y " << initialY << " target " << targetY
<< " gives gain " << gain;
if (extraGain < 1.01) /* close enough */
break;
}
for (auto &c : *constraintMode_) {
double newTargetY;
double newGain = constraintComputeGain(c, h, lux.lux, evGain, newTargetY);
LOG(RPiAgc, Debug) << "Constraint has target_Y "
<< newTargetY << " giving gain " << newGain;
if (c.bound == AgcConstraint::Bound::LOWER && newGain > gain) {
LOG(RPiAgc, Debug) << "Lower bound constraint adopted";
gain = newGain;
targetY = newTargetY;
} else if (c.bound == AgcConstraint::Bound::UPPER && newGain < gain) {
LOG(RPiAgc, Debug) << "Upper bound constraint adopted";
gain = newGain;
targetY = newTargetY;
}
}
LOG(RPiAgc, Debug) << "Final gain " << gain << " (target_Y " << targetY << " ev "
<< status_.ev << " base_ev " << config_.baseEv
<< ")";
}
void Agc::computeTargetExposure(double gain)
{
if (status_.fixedShutter && status_.fixedAnalogueGain) {
/*
* When ag and shutter are both fixed, we need to drive the
* total exposure so that we end up with a digital gain of at least
* 1/minColourGain. Otherwise we'd desaturate channels causing
* white to go cyan or magenta.
*/
double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });
ASSERT(minColourGain != 0.0);
target_.totalExposure =
status_.fixedShutter * status_.fixedAnalogueGain / minColourGain;
} else {
/*
* The statistics reflect the image without digital gain, so the final
* total exposure we're aiming for is:
*/
target_.totalExposure = current_.totalExposureNoDG * gain;
/* The final target exposure is also limited to what the exposure mode allows. */
Duration maxShutter = status_.fixedShutter
? status_.fixedShutter
: exposureMode_->shutter.back();
maxShutter = clipShutter(maxShutter);
Duration maxTotalExposure =
maxShutter *
(status_.fixedAnalogueGain != 0.0
? status_.fixedAnalogueGain
: exposureMode_->gain.back());
target_.totalExposure = std::min(target_.totalExposure, maxTotalExposure);
}
LOG(RPiAgc, Debug) << "Target totalExposure " << target_.totalExposure;
}
bool Agc::applyDigitalGain(double gain, double targetY)
{
double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });
ASSERT(minColourGain != 0.0);
double dg = 1.0 / minColourGain;
/*
* I think this pipeline subtracts black level and rescales before we
* get the stats, so no need to worry about it.
*/
LOG(RPiAgc, Debug) << "after AWB, target dg " << dg << " gain " << gain
<< " target_Y " << targetY;
/*
* Finally, if we're trying to reduce exposure but the target_Y is
* "close" to 1.0, then the gain computed for that constraint will be
* only slightly less than one, because the measured Y can never be
* larger than 1.0. When this happens, demand a large digital gain so
* that the exposure can be reduced, de-saturating the image much more
* quickly (and we then approach the correct value more quickly from
* below).
*/
bool desaturate = targetY > config_.fastReduceThreshold &&
gain < sqrt(targetY);
if (desaturate)
dg /= config_.fastReduceThreshold;
LOG(RPiAgc, Debug) << "Digital gain " << dg << " desaturate? " << desaturate;
target_.totalExposureNoDG = target_.totalExposure / dg;
LOG(RPiAgc, Debug) << "Target totalExposureNoDG " << target_.totalExposureNoDG;
return desaturate;
}
void Agc::filterExposure(bool desaturate)
{
double speed = config_.speed;
/*
* AGC adapts instantly if both shutter and gain are directly specified
* or we're in the startup phase.
*/
if ((status_.fixedShutter && status_.fixedAnalogueGain) ||
frameCount_ <= config_.startupFrames)
speed = 1.0;
if (!filtered_.totalExposure) {
filtered_.totalExposure = target_.totalExposure;
filtered_.totalExposureNoDG = target_.totalExposureNoDG;
} else {
/*
* If close to the result go faster, to save making so many
* micro-adjustments on the way. (Make this customisable?)
*/
if (filtered_.totalExposure < 1.2 * target_.totalExposure &&
filtered_.totalExposure > 0.8 * target_.totalExposure)
speed = sqrt(speed);
filtered_.totalExposure = speed * target_.totalExposure +
filtered_.totalExposure * (1.0 - speed);
/*
* When desaturing, take a big jump down in totalExposureNoDG,
* which we'll hide with digital gain.
*/
if (desaturate)
filtered_.totalExposureNoDG =
target_.totalExposureNoDG;
else
filtered_.totalExposureNoDG =
speed * target_.totalExposureNoDG +
filtered_.totalExposureNoDG * (1.0 - speed);
}
/*
* We can't let the totalExposureNoDG exposure deviate too far below the
* total exposure, as there might not be enough digital gain available
* in the ISP to hide it (which will cause nasty oscillation).
*/
if (filtered_.totalExposureNoDG <
filtered_.totalExposure * config_.fastReduceThreshold)
filtered_.totalExposureNoDG = filtered_.totalExposure * config_.fastReduceThreshold;
LOG(RPiAgc, Debug) << "After filtering, totalExposure " << filtered_.totalExposure
<< " no dg " << filtered_.totalExposureNoDG;
}
void Agc::divideUpExposure()
{
/*
* Sending the fixed shutter/gain cases through the same code may seem
* unnecessary, but it will make more sense when extend this to cover
* variable aperture.
*/
Duration exposureValue = filtered_.totalExposureNoDG;
Duration shutterTime;
double analogueGain;
shutterTime = status_.fixedShutter ? status_.fixedShutter
: exposureMode_->shutter[0];
shutterTime = clipShutter(shutterTime);
analogueGain = status_.fixedAnalogueGain != 0.0 ? status_.fixedAnalogueGain
: exposureMode_->gain[0];
if (shutterTime * analogueGain < exposureValue) {
for (unsigned int stage = 1;
stage < exposureMode_->gain.size(); stage++) {
if (!status_.fixedShutter) {
Duration stageShutter =
clipShutter(exposureMode_->shutter[stage]);
if (stageShutter * analogueGain >= exposureValue) {
shutterTime = exposureValue / analogueGain;
break;
}
shutterTime = stageShutter;
}
if (status_.fixedAnalogueGain == 0.0) {
if (exposureMode_->gain[stage] * shutterTime >= exposureValue) {
analogueGain = exposureValue / shutterTime;
break;
}
analogueGain = exposureMode_->gain[stage];
}
}
}
LOG(RPiAgc, Debug) << "Divided up shutter and gain are " << shutterTime << " and "
<< analogueGain;
/*
* Finally adjust shutter time for flicker avoidance (require both
* shutter and gain not to be fixed).
*/
if (!status_.fixedShutter && !status_.fixedAnalogueGain &&
status_.flickerPeriod) {
int flickerPeriods = shutterTime / status_.flickerPeriod;
if (flickerPeriods) {
Duration newShutterTime = flickerPeriods * status_.flickerPeriod;
analogueGain *= shutterTime / newShutterTime;
/*
* We should still not allow the ag to go over the
* largest value in the exposure mode. Note that this
* may force more of the total exposure into the digital
* gain as a side-effect.
*/
analogueGain = std::min(analogueGain, exposureMode_->gain.back());
shutterTime = newShutterTime;
}
LOG(RPiAgc, Debug) << "After flicker avoidance, shutter "
<< shutterTime << " gain " << analogueGain;
}
filtered_.shutter = shutterTime;
filtered_.analogueGain = analogueGain;
}
void Agc::writeAndFinish(Metadata *imageMetadata, bool desaturate)
{
status_.totalExposureValue = filtered_.totalExposure;
status_.targetExposureValue = desaturate ? 0s : target_.totalExposureNoDG;
status_.shutterTime = filtered_.shutter;
status_.analogueGain = filtered_.analogueGain;
/*
* Write to metadata as well, in case anyone wants to update the camera
* immediately.
*/
imageMetadata->set("agc.status", status_);
LOG(RPiAgc, Debug) << "Output written, total exposure requested is "
<< filtered_.totalExposure;
LOG(RPiAgc, Debug) << "Camera exposure update: shutter time " << filtered_.shutter
<< " analogue gain " << filtered_.analogueGain;
}
Duration Agc::clipShutter(Duration shutter)
{
if (maxShutter_)
shutter = std::min(shutter, maxShutter_);
return shutter;
}
/* Register algorithm with the system. */
static Algorithm *create(Controller *controller)
{
return (Algorithm *)new Agc(controller);
}
static RegisterAlgorithm reg(NAME, &create);