[libcamera-devel] [PATCH v3 2/5] ipa: rpi: agc: Reorganise code for multi-channel AGC

David Plowman david.plowman at raspberrypi.com
Fri Sep 15 10:08:08 CEST 2023


Hi Jacopo

Thanks for the review again!

On Tue, 12 Sept 2023 at 15:46, Jacopo Mondi
<jacopo.mondi at ideasonboard.com> wrote:
>
> Hi David
>
> On Tue, Sep 12, 2023 at 11:24:39AM +0100, David Plowman via libcamera-devel wrote:
> > This commit does the basic reorganisation of the code in order to
> > implement multi-channel AGC. The main changes are:
> >
> > * The previous Agc class (in agc.cpp) has become the AgcChannel class
> >   in (agc_channel.cpp).
> >
> > * A new Agc class is introduced which is a wrapper round a number of
> >   AgcChannels.
> >
> > * The basic plumbing from ipa_base.cpp to Agc is updated to include a
> >   channel number. All the existing controls are hardwired to talk
> >   directly to channel 0.
> >
> > There are a couple of limitations which we expect to apply to
> > multi-channel AGC. We're not allowing different frame durations to be
> > applied to the channels, nor are we allowing separate metering
> > modes. To be fair, supporting these things is not impossible, but
> > there are reasons why it may be tricky so they remain "TBD" for now.
> >
> > This patch only includes the basic reorganisation and plumbing. It
> > does not yet update the important methods (switchMode, prepare and
> > process) to implement multi-channel AGC properly. This will appear in
> > a subsequent commit. For now, these functions are hard-coded just to
> > use channel 0, thereby preserving the existing behaviour.
> >
> > Signed-off-by: David Plowman <david.plowman at raspberrypi.com>
> > Reviewed-by: Naushir Patuck <naush at raspberrypi.com>
> > ---
> >  src/ipa/rpi/common/ipa_base.cpp            |  20 +-
> >  src/ipa/rpi/controller/agc_algorithm.h     |  19 +-
> >  src/ipa/rpi/controller/meson.build         |   1 +
> >  src/ipa/rpi/controller/rpi/agc.cpp         | 912 +++-----------------
> >  src/ipa/rpi/controller/rpi/agc.h           | 121 +--
> >  src/ipa/rpi/controller/rpi/agc_channel.cpp | 924 +++++++++++++++++++++
> >  src/ipa/rpi/controller/rpi/agc_channel.h   | 137 +++
> >  7 files changed, 1219 insertions(+), 915 deletions(-)
> >  create mode 100644 src/ipa/rpi/controller/rpi/agc_channel.cpp
> >  create mode 100644 src/ipa/rpi/controller/rpi/agc_channel.h
> >
> > diff --git a/src/ipa/rpi/common/ipa_base.cpp b/src/ipa/rpi/common/ipa_base.cpp
> > index a47ae3a9..f7e7ad5e 100644
> > --- a/src/ipa/rpi/common/ipa_base.cpp
> > +++ b/src/ipa/rpi/common/ipa_base.cpp
> > @@ -699,9 +699,9 @@ void IpaBase::applyControls(const ControlList &controls)
> >                       }
> >
> >                       if (ctrl.second.get<bool>() == false)
> > -                             agc->disableAuto();
> > +                             agc->disableAuto(0);
> >                       else
> > -                             agc->enableAuto();
> > +                             agc->enableAuto(0);
> >
> >                       libcameraMetadata_.set(controls::AeEnable, ctrl.second.get<bool>());
> >                       break;
> > @@ -717,7 +717,7 @@ void IpaBase::applyControls(const ControlList &controls)
> >                       }
> >
> >                       /* The control provides units of microseconds. */
> > -                     agc->setFixedShutter(ctrl.second.get<int32_t>() * 1.0us);
> > +                     agc->setFixedShutter(0, ctrl.second.get<int32_t>() * 1.0us);
> >
> >                       libcameraMetadata_.set(controls::ExposureTime, ctrl.second.get<int32_t>());
> >                       break;
> > @@ -732,7 +732,7 @@ void IpaBase::applyControls(const ControlList &controls)
> >                               break;
> >                       }
> >
> > -                     agc->setFixedAnalogueGain(ctrl.second.get<float>());
> > +                     agc->setFixedAnalogueGain(0, ctrl.second.get<float>());
> >
> >                       libcameraMetadata_.set(controls::AnalogueGain,
> >                                              ctrl.second.get<float>());
> > @@ -770,7 +770,7 @@ void IpaBase::applyControls(const ControlList &controls)
> >
> >                       int32_t idx = ctrl.second.get<int32_t>();
> >                       if (ConstraintModeTable.count(idx)) {
> > -                             agc->setConstraintMode(ConstraintModeTable.at(idx));
> > +                             agc->setConstraintMode(0, ConstraintModeTable.at(idx));
> >                               libcameraMetadata_.set(controls::AeConstraintMode, idx);
> >                       } else {
> >                               LOG(IPARPI, Error) << "Constraint mode " << idx
> > @@ -790,7 +790,7 @@ void IpaBase::applyControls(const ControlList &controls)
> >
> >                       int32_t idx = ctrl.second.get<int32_t>();
> >                       if (ExposureModeTable.count(idx)) {
> > -                             agc->setExposureMode(ExposureModeTable.at(idx));
> > +                             agc->setExposureMode(0, ExposureModeTable.at(idx));
> >                               libcameraMetadata_.set(controls::AeExposureMode, idx);
> >                       } else {
> >                               LOG(IPARPI, Error) << "Exposure mode " << idx
> > @@ -813,7 +813,7 @@ void IpaBase::applyControls(const ControlList &controls)
> >                        * So convert to 2^EV
> >                        */
> >                       double ev = pow(2.0, ctrl.second.get<float>());
> > -                     agc->setEv(ev);
> > +                     agc->setEv(0, ev);
> >                       libcameraMetadata_.set(controls::ExposureValue,
> >                                              ctrl.second.get<float>());
> >                       break;
> > @@ -833,12 +833,12 @@ void IpaBase::applyControls(const ControlList &controls)
> >
> >                       switch (mode) {
> >                       case controls::FlickerOff:
> > -                             agc->setFlickerPeriod(0us);
> > +                             agc->setFlickerPeriod(0, 0us);
> >
> >                               break;
> >
> >                       case controls::FlickerManual:
> > -                             agc->setFlickerPeriod(flickerState_.manualPeriod);
> > +                             agc->setFlickerPeriod(0, flickerState_.manualPeriod);
> >
> >                               break;
> >
> > @@ -872,7 +872,7 @@ void IpaBase::applyControls(const ControlList &controls)
> >                        * first, and the period updated after, or vice versa.
> >                        */
> >                       if (flickerState_.mode == controls::FlickerManual)
> > -                             agc->setFlickerPeriod(flickerState_.manualPeriod);
> > +                             agc->setFlickerPeriod(0, flickerState_.manualPeriod);
> >
> >                       break;
> >               }
> > diff --git a/src/ipa/rpi/controller/agc_algorithm.h b/src/ipa/rpi/controller/agc_algorithm.h
> > index b6949daa..b8986560 100644
> > --- a/src/ipa/rpi/controller/agc_algorithm.h
> > +++ b/src/ipa/rpi/controller/agc_algorithm.h
> > @@ -21,16 +21,19 @@ public:
> >       /* An AGC algorithm must provide the following: */
> >       virtual unsigned int getConvergenceFrames() const = 0;
> >       virtual std::vector<double> const &getWeights() const = 0;
> > -     virtual void setEv(double ev) = 0;
> > -     virtual void setFlickerPeriod(libcamera::utils::Duration flickerPeriod) = 0;
> > -     virtual void setFixedShutter(libcamera::utils::Duration fixedShutter) = 0;
> > +     virtual void setEv(unsigned int channel, double ev) = 0;
> > +     virtual void setFlickerPeriod(unsigned int channel,
> > +                                   libcamera::utils::Duration flickerPeriod) = 0;
> > +     virtual void setFixedShutter(unsigned int channel,
> > +                                  libcamera::utils::Duration fixedShutter) = 0;
> >       virtual void setMaxShutter(libcamera::utils::Duration maxShutter) = 0;
> > -     virtual void setFixedAnalogueGain(double fixedAnalogueGain) = 0;
> > +     virtual void setFixedAnalogueGain(unsigned int channel, double fixedAnalogueGain) = 0;
> >       virtual void setMeteringMode(std::string const &meteringModeName) = 0;
> > -     virtual void setExposureMode(std::string const &exposureModeName) = 0;
> > -     virtual void setConstraintMode(std::string const &contraintModeName) = 0;
> > -     virtual void enableAuto() = 0;
> > -     virtual void disableAuto() = 0;
> > +     virtual void setExposureMode(unsigned int channel, std::string const &exposureModeName) = 0;
> > +     virtual void setConstraintMode(unsigned int channel, std::string const &contraintModeName) = 0;
> > +     virtual void enableAuto(unsigned int channel) = 0;
> > +     virtual void disableAuto(unsigned int channel) = 0;
> > +     virtual void setActiveChannels(const std::vector<unsigned int> &activeChannels) = 0;
> >  };
> >
> >  } /* namespace RPiController */
> > diff --git a/src/ipa/rpi/controller/meson.build b/src/ipa/rpi/controller/meson.build
> > index feb0334e..20b9cda9 100644
> > --- a/src/ipa/rpi/controller/meson.build
> > +++ b/src/ipa/rpi/controller/meson.build
> > @@ -8,6 +8,7 @@ rpi_ipa_controller_sources = files([
> >      'pwl.cpp',
> >      'rpi/af.cpp',
> >      'rpi/agc.cpp',
> > +    'rpi/agc_channel.cpp',
> >      'rpi/alsc.cpp',
> >      'rpi/awb.cpp',
> >      'rpi/black_level.cpp',
> > diff --git a/src/ipa/rpi/controller/rpi/agc.cpp b/src/ipa/rpi/controller/rpi/agc.cpp
> > index 7b02972a..598fc890 100644
> > --- a/src/ipa/rpi/controller/rpi/agc.cpp
> > +++ b/src/ipa/rpi/controller/rpi/agc.cpp
> > @@ -5,20 +5,12 @@
> >   * agc.cpp - AGC/AEC control algorithm
> >   */
> >
> > -#include <algorithm>
> > -#include <map>
> > -#include <tuple>
> > +#include "agc.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;
> > @@ -28,881 +20,205 @@ LOG_DEFINE_CATEGORY(RPiAgc)
> >
> >  #define NAME "rpi.agc"
> >
> > -int AgcMeteringMode::read(const libcamera::YamlObject &params)
> > +Agc::Agc(Controller *controller)
> > +     : AgcAlgorithm(controller),
> > +       activeChannels_({ 0 })
> >  {
> > -     const YamlObject &yamlWeights = params["weights"];
> > -
> > -     for (const auto &p : yamlWeights.asList()) {
> > -             auto value = p.get<double>();
> > -             if (!value)
> > -                     return -EINVAL;
> > -             weights.push_back(*value);
> > -     }
> > -
> > -     return 0;
> >  }
> >
> > -static std::tuple<int, std::string>
> > -readMeteringModes(std::map<std::string, AgcMeteringMode> &metering_modes,
> > -               const libcamera::YamlObject &params)
> > +char const *Agc::name() const
> >  {
> > -     std::string first;
> > -     int ret;
> > -
> > -     for (const auto &[key, value] : params.asDict()) {
> > -             AgcMeteringMode meteringMode;
> > -             ret = meteringMode.read(value);
> > -             if (ret)
> > -                     return { ret, {} };
> > -
> > -             metering_modes[key] = std::move(meteringMode);
> > -             if (first.empty())
> > -                     first = key;
> > -     }
> > -
> > -     return { 0, first };
> > +     return NAME;
> >  }
> >
> > -int AgcExposureMode::read(const libcamera::YamlObject &params)
> > +int Agc::read(const libcamera::YamlObject &params)
> >  {
> > -     auto value = params["shutter"].getList<double>();
> > -     if (!value)
> > -             return -EINVAL;
> > -     std::transform(value->begin(), value->end(), std::back_inserter(shutter),
> > -                    [](double v) { return v * 1us; });
> > -
> > -     value = params["gain"].getList<double>();
> > -     if (!value)
> > -             return -EINVAL;
> > -     gain = std::move(*value);
> > -
> > -     if (shutter.size() < 2 || gain.size() < 2) {
> > -             LOG(RPiAgc, Error)
> > -                     << "AgcExposureMode: must have at least two entries in exposure profile";
> > -             return -EINVAL;
> > -     }
> > -
> > -     if (shutter.size() != gain.size()) {
> > -             LOG(RPiAgc, Error)
> > -                     << "AgcExposureMode: expect same number of exposure and gain entries in exposure profile";
> > -             return -EINVAL;
> > +     /*
> > +      * When there is only a single channel we can read the old style syntax.
> > +      * Otherwise we expect a "channels" keyword followed by a list of configurations.
> > +      */
> > +     if (!params.contains("channels")) {
> > +             LOG(RPiAgc, Debug) << "Single channel only";
> > +             channelData_.emplace_back();
> > +             return channelData_.back().channel.read(params, getHardwareConfig());
> >       }
> >
> > -     return 0;
> > -}
> > -
> > -static std::tuple<int, std::string>
> > -readExposureModes(std::map<std::string, AgcExposureMode> &exposureModes,
> > -               const libcamera::YamlObject &params)
> > -{
> > -     std::string first;
> > -     int ret;
> > -
> > -     for (const auto &[key, value] : params.asDict()) {
> > -             AgcExposureMode exposureMode;
> > -             ret = exposureMode.read(value);
> > +     const auto &channels = params["channels"].asList();
> > +     for (auto ch = channels.begin(); ch != channels.end(); ch++) {
> > +             LOG(RPiAgc, Debug) << "Read AGC channel";
> > +             channelData_.emplace_back();
> > +             int ret = channelData_.back().channel.read(*ch, getHardwareConfig());
> >               if (ret)
> > -                     return { ret, {} };
> > -
> > -             exposureModes[key] = std::move(exposureMode);
> > -             if (first.empty())
> > -                     first = key;
> > +                     return ret;
> >       }
> >
> > -     return { 0, first };
> > -}
> > -
> > -int AgcConstraint::read(const libcamera::YamlObject &params)
> > -{
> > -     std::string boundString = params["bound"].get<std::string>("");
> > -     transform(boundString.begin(), boundString.end(),
> > -               boundString.begin(), ::toupper);
> > -     if (boundString != "UPPER" && boundString != "LOWER") {
> > -             LOG(RPiAgc, Error) << "AGC constraint type should be UPPER or LOWER";
> > -             return -EINVAL;
> > +     LOG(RPiAgc, Debug) << "Read " << channelData_.size() << " channel(s)";
> > +     if (channelData_.empty()) {
> > +             LOG(RPiAgc, Error) << "No AGC channels provided";
> > +             return -1;
> >       }
> > -     bound = boundString == "UPPER" ? Bound::UPPER : Bound::LOWER;
> > -
> > -     auto value = params["q_lo"].get<double>();
> > -     if (!value)
> > -             return -EINVAL;
> > -     qLo = *value;
> > -
> > -     value = params["q_hi"].get<double>();
> > -     if (!value)
> > -             return -EINVAL;
> > -     qHi = *value;
> > -
> > -     return yTarget.read(params["y_target"]);
> > -}
> >
> > -static std::tuple<int, AgcConstraintMode>
> > -readConstraintMode(const libcamera::YamlObject &params)
> > -{
> > -     AgcConstraintMode mode;
> > -     int ret;
> > -
> > -     for (const auto &p : params.asList()) {
> > -             AgcConstraint constraint;
> > -             ret = constraint.read(p);
> > -             if (ret)
> > -                     return { ret, {} };
> > -
> > -             mode.push_back(std::move(constraint));
> > -     }
> > -
> > -     return { 0, mode };
> > +     return 0;
> >  }
> >
> > -static std::tuple<int, std::string>
> > -readConstraintModes(std::map<std::string, AgcConstraintMode> &constraintModes,
> > -                 const libcamera::YamlObject &params)
> > +int Agc::checkChannel(unsigned int channelIndex) const
> >  {
> > -     std::string first;
> > -     int ret;
> > -
> > -     for (const auto &[key, value] : params.asDict()) {
> > -             std::tie(ret, constraintModes[key]) = readConstraintMode(value);
> > -             if (ret)
> > -                     return { ret, {} };
> > -
> > -             if (first.empty())
> > -                     first = key;
> > +     if (channelIndex >= channelData_.size()) {
> > +             LOG(RPiAgc, Warning) << "AGC channel " << channelIndex << " not available";
> > +             return -1;
> >       }
> >
> > -     return { 0, first };
> > -}
> > -
> > -int AgcConfig::read(const libcamera::YamlObject &params)
> > -{
> > -     LOG(RPiAgc, Debug) << "AgcConfig";
> > -     int ret;
> > -
> > -     std::tie(ret, defaultMeteringMode) =
> > -             readMeteringModes(meteringModes, params["metering_modes"]);
> > -     if (ret)
> > -             return ret;
> > -     std::tie(ret, defaultExposureMode) =
> > -             readExposureModes(exposureModes, params["exposure_modes"]);
> > -     if (ret)
> > -             return ret;
> > -     std::tie(ret, defaultConstraintMode) =
> > -             readConstraintModes(constraintModes, params["constraint_modes"]);
> > -     if (ret)
> > -             return ret;
> > -
> > -     ret = yTarget.read(params["y_target"]);
> > -     if (ret)
> > -             return ret;
> > -
> > -     speed = params["speed"].get<double>(0.2);
> > -     startupFrames = params["startup_frames"].get<uint16_t>(10);
> > -     convergenceFrames = params["convergence_frames"].get<unsigned int>(6);
> > -     fastReduceThreshold = params["fast_reduce_threshold"].get<double>(0.4);
> > -     baseEv = params["base_ev"].get<double>(1.0);
> > -
> > -     /* Start with quite a low value as ramping up is easier than ramping down. */
> > -     defaultExposureTime = params["default_exposure_time"].get<double>(1000) * 1us;
> > -     defaultAnalogueGain = params["default_analogue_gain"].get<double>(1.0);
> > -
> >       return 0;
> >  }
> >
> > -Agc::ExposureValues::ExposureValues()
> > -     : shutter(0s), analogueGain(0),
> > -       totalExposure(0s), totalExposureNoDG(0s)
> > +void Agc::disableAuto(unsigned int channelIndex)
> >  {
> > -}
> > -
> > -Agc::Agc(Controller *controller)
> > -     : AgcAlgorithm(controller), meteringMode_(nullptr),
> > -       exposureMode_(nullptr), constraintMode_(nullptr),
> > -       frameCount_(0), lockCount_(0),
> > -       lastTargetExposure_(0s), 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).
> > -      */
> > -     status_ = {};
> > -     status_.ev = ev_;
> > -}
> > +     if (checkChannel(channelIndex))
> > +             return;
> >
> > -char const *Agc::name() const
> > -{
> > -     return NAME;
> > +     LOG(RPiAgc, Debug) << "disableAuto for channel " << channelIndex;
> > +     channelData_[channelIndex].channel.disableAuto();
> >  }
> >
> > -int Agc::read(const libcamera::YamlObject &params)
> > +void Agc::enableAuto(unsigned int channelIndex)
> >  {
> > -     LOG(RPiAgc, Debug) << "Agc";
> > -
> > -     int ret = config_.read(params);
> > -     if (ret)
> > -             return ret;
> > -
> > -     const Size &size = getHardwareConfig().agcZoneWeights;
> > -     for (auto const &modes : config_.meteringModes) {
> > -             if (modes.second.weights.size() != size.width * size.height) {
> > -                     LOG(RPiAgc, Error) << "AgcMeteringMode: Incorrect number of weights";
> > -                     return -EINVAL;
> > -             }
> > -     }
> > +     if (checkChannel(channelIndex))
> > +             return;
> >
> > -     /*
> > -      * 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;
> > -     return 0;
> > -}
> > -
> > -void Agc::disableAuto()
> > -{
> > -     fixedShutter_ = status_.shutterTime;
> > -     fixedAnalogueGain_ = status_.analogueGain;
> > -}
> > -
> > -void Agc::enableAuto()
> > -{
> > -     fixedShutter_ = 0s;
> > -     fixedAnalogueGain_ = 0;
> > +     LOG(RPiAgc, Debug) << "enableAuto for channel " << channelIndex;
> > +     channelData_[channelIndex].channel.enableAuto();
> >  }
> >
> >  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;
> > +     /* If there are n channels, it presumably takes n times as long to converge. */
> > +     return channelData_[0].channel.getConvergenceFrames() * activeChannels_.size();
> >  }
> >
> >  std::vector<double> const &Agc::getWeights() const
> >  {
> >       /*
> > -      * In case someone calls setMeteringMode and then this before the
> > -      * algorithm has run and updated the meteringMode_ pointer.
> > +      * In future the metering weights may be determined differently, making it
> > +      * difficult to associate different sets of weight with different channels.
> > +      * Therefore we shall impose a limitation, at least for now, that all
> > +      * channels will use the same weights.
> >        */
> > -     auto it = config_.meteringModes.find(meteringModeName_);
> > -     if (it == config_.meteringModes.end())
> > -             return meteringMode_->weights;
> > -     return it->second.weights;
> > +     return channelData_[0].channel.getWeights();
> >  }
> >
> > -void Agc::setEv(double ev)
> > +void Agc::setEv(unsigned int channelIndex, double ev)
> >  {
> > -     ev_ = ev;
> > -}
> > +     if (checkChannel(channelIndex))
> > +             return;
> >
> > -void Agc::setFlickerPeriod(Duration flickerPeriod)
> > -{
> > -     flickerPeriod_ = flickerPeriod;
> > +     LOG(RPiAgc, Debug) << "setEv " << ev << " for channel " << channelIndex;
> > +     channelData_[channelIndex].channel.setEv(ev);
> >  }
> >
> > -void Agc::setMaxShutter(Duration maxShutter)
> > +void Agc::setFlickerPeriod(unsigned int channelIndex, Duration flickerPeriod)
> >  {
> > -     maxShutter_ = maxShutter;
> > -}
> > +     if (checkChannel(channelIndex))
> > +             return;
> >
> > -void Agc::setFixedShutter(Duration fixedShutter)
> > -{
> > -     fixedShutter_ = fixedShutter;
> > -     /* Set this in case someone calls disableAuto() straight after. */
> > -     status_.shutterTime = limitShutter(fixedShutter_);
> > +     LOG(RPiAgc, Debug) << "setFlickerPeriod " << flickerPeriod
> > +                        << " for channel " << channelIndex;
> > +     channelData_[channelIndex].channel.setFlickerPeriod(flickerPeriod);
> >  }
> >
> > -void Agc::setFixedAnalogueGain(double fixedAnalogueGain)
> > -{
> > -     fixedAnalogueGain_ = fixedAnalogueGain;
> > -     /* Set this in case someone calls disableAuto() straight after. */
> > -     status_.analogueGain = limitGain(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)
> > +void Agc::setMaxShutter(Duration maxShutter)
> >  {
> > -     /* AGC expects the mode sensitivity always to be non-zero. */
> > -     ASSERT(cameraMode.sensitivity);
> > -
> > -     housekeepConfig();
> > -
> > -     /*
> > -      * Store the mode in the local state. We must cache the sensitivity of
> > -      * of the previous mode for the calculations below.
> > -      */
> > -     double lastSensitivity = mode_.sensitivity;
> > -     mode_ = cameraMode;
> > -
> > -     Duration fixedShutter = limitShutter(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);
> > +     /* Frame durations will be the same across all channels too. */
> > +     for (auto &data : channelData_)
> > +             data.channel.setMaxShutter(maxShutter);
> >  }
> >
> > -void Agc::prepare(Metadata *imageMetadata)
> > +void Agc::setFixedShutter(unsigned int channelIndex, Duration fixedShutter)
> >  {
> > -     Duration totalExposureValue = status_.totalExposureValue;
> > -     AgcStatus delayedStatus;
> > -     AgcPrepareStatus prepareStatus;
> > -
> > -     if (!imageMetadata->get("agc.delayed_status", delayedStatus))
> > -             totalExposureValue = delayedStatus.totalExposureValue;
> > -
> > -     prepareStatus.digitalGain = 1.0;
> > -     prepareStatus.locked = false;
> > -
> > -     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) {
> > -                             double digitalGain = totalExposureValue / actualExposure;
> > -                             LOG(RPiAgc, Debug) << "Want total exposure " << totalExposureValue;
> > -                             /*
> > -                              * Never ask for a gain < 1.0, and also impose
> > -                              * some upper limit. Make it customisable?
> > -                              */
> > -                             prepareStatus.digitalGain = std::max(1.0, std::min(digitalGain, 4.0));
> > -                             LOG(RPiAgc, Debug) << "Actual exposure " << actualExposure;
> > -                             LOG(RPiAgc, Debug) << "Use digitalGain " << prepareStatus.digitalGain;
> > -                             LOG(RPiAgc, Debug) << "Effective exposure "
> > -                                                << actualExposure * prepareStatus.digitalGain;
> > -                             /* Decide whether AEC/AGC has converged. */
> > -                             prepareStatus.locked = updateLockStatus(deviceStatus);
> > -                     }
> > -             } else
> > -                     LOG(RPiAgc, Warning) << name() << ": no device metadata";
> > -             imageMetadata->set("agc.prepare_status", prepareStatus);
> > -     }
> > -}
> > +     if (checkChannel(channelIndex))
> > +             return;
> >
> > -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();
> > -     /* Fetch the AWB status immediately, so that we can assume it's there. */
> > -     fetchAwbStatus(imageMetadata);
> > -     /* 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, imageMetadata, gain, targetY);
> > -     /* Now compute the target (final) exposure which we think we want. */
> > -     computeTargetExposure(gain);
> > -     /* The results have to be filtered so as not to change too rapidly. */
> > -     filterExposure();
> > -     /*
> > -      * 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 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);
> > +     LOG(RPiAgc, Debug) << "setFixedShutter " << fixedShutter
> > +                        << " for channel " << channelIndex;
> > +     channelData_[channelIndex].channel.setFixedShutter(fixedShutter);
> >  }
> >
> > -bool Agc::updateLockStatus(DeviceStatus const &deviceStatus)
> > +void Agc::setFixedAnalogueGain(unsigned int channelIndex, double fixedAnalogueGain)
> >  {
> > -     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;
> > +     if (checkChannel(channelIndex))
> > +             return;
> >
> > -     /* 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_;
> > -     return lockCount_ == maxLockCount;
> > +     LOG(RPiAgc, Debug) << "setFixedAnalogueGain " << fixedAnalogueGain
> > +                        << " for channel " << channelIndex;
> > +     channelData_[channelIndex].channel.setFixedAnalogueGain(fixedAnalogueGain);
> >  }
> >
> > -void Agc::housekeepConfig()
> > +void Agc::setMeteringMode(std::string const &meteringModeName)
> >  {
> > -     /* First fetch all the up-to-date settings, so no one else has to do it. */
> > -     status_.ev = ev_;
> > -     status_.fixedShutter = limitShutter(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 (meteringModeName_ != status_.meteringMode) {
> > -             auto it = config_.meteringModes.find(meteringModeName_);
> > -             if (it == config_.meteringModes.end()) {
> > -                     LOG(RPiAgc, Warning) << "No metering mode " << meteringModeName_;
> > -                     meteringModeName_ = status_.meteringMode;
> > -             } else {
> > -                     meteringMode_ = &it->second;
> > -                     status_.meteringMode = meteringModeName_;
> > -             }
> > -     }
> > -     if (exposureModeName_ != status_.exposureMode) {
> > -             auto it = config_.exposureModes.find(exposureModeName_);
> > -             if (it == config_.exposureModes.end()) {
> > -                     LOG(RPiAgc, Warning) << "No exposure profile " << exposureModeName_;
> > -                     exposureModeName_ = status_.exposureMode;
> > -             } else {
> > -                     exposureMode_ = &it->second;
> > -                     status_.exposureMode = exposureModeName_;
> > -             }
> > -     }
> > -     if (constraintModeName_ != status_.constraintMode) {
> > -             auto it = config_.constraintModes.find(constraintModeName_);
> > -             if (it == config_.constraintModes.end()) {
> > -                     LOG(RPiAgc, Warning) << "No constraint list " << constraintModeName_;
> > -                     constraintModeName_ = status_.constraintMode;
> > -             } else {
> > -                     constraintMode_ = &it->second;
> > -                     status_.constraintMode = constraintModeName_;
> > -             }
> > -     }
> > -     LOG(RPiAgc, Debug) << "exposureMode "
> > -                        << exposureModeName_ << " constraintMode "
> > -                        << constraintModeName_ << " meteringMode "
> > -                        << meteringModeName_;
> > +     /* Metering modes will be the same across all channels too. */
> > +     for (auto &data : channelData_)
> > +             data.channel.setMeteringMode(meteringModeName);
> >  }
> >
> > -void Agc::fetchCurrentExposure(Metadata *imageMetadata)
> > +void Agc::setExposureMode(unsigned int channelIndex, std::string const &exposureModeName)
> >  {
> > -     std::unique_lock<Metadata> lock(*imageMetadata);
> > -     DeviceStatus *deviceStatus =
> > -             imageMetadata->getLocked<DeviceStatus>("device.status");
> > -     if (!deviceStatus)
> > -             LOG(RPiAgc, Fatal) << "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;
> > -}
> > +     if (checkChannel(channelIndex))
> > +             return;
> >
> > -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) << "No AWB status found";
> > +     LOG(RPiAgc, Debug) << "setExposureMode " << exposureModeName
> > +                        << " for channel " << channelIndex;
> > +     channelData_[channelIndex].channel.setExposureMode(exposureModeName);
> >  }
> >
> > -static double computeInitialY(StatisticsPtr &stats, AwbStatus const &awb,
> > -                           std::vector<double> &weights, double gain)
> > +void Agc::setConstraintMode(unsigned int channelIndex, std::string const &constraintModeName)
> >  {
> > -     constexpr uint64_t maxVal = 1 << Statistics::NormalisationFactorPow2;
> > +     if (checkChannel(channelIndex))
> > +             return;
> >
> > -     ASSERT(weights.size() == stats->agcRegions.numRegions());
> > -
> > -     /*
> > -      * Note that the weights are applied by the IPA to the statistics directly,
> > -      * before they are given to us here.
> > -      */
> > -     double rSum = 0, gSum = 0, bSum = 0, pixelSum = 0;
> > -     for (unsigned int i = 0; i < stats->agcRegions.numRegions(); i++) {
> > -             auto &region = stats->agcRegions.get(i);
> > -             rSum += std::min<double>(region.val.rSum * gain, (maxVal - 1) * region.counted);
> > -             gSum += std::min<double>(region.val.gSum * gain, (maxVal - 1) * region.counted);
> > -             bSum += std::min<double>(region.val.bSum * gain, (maxVal - 1) * region.counted);
> > -             pixelSum += region.counted;
> > -     }
> > -     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 / maxVal;
> > +     channelData_[channelIndex].channel.setConstraintMode(constraintModeName);
> >  }
> >
> > -/*
> > - * 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.
> > - */
> > -
> > -static constexpr double EvGainYTargetLimit = 0.9;
> > -
> > -static double constraintComputeGain(AgcConstraint &c, const Histogram &h, double lux,
> > -                                 double evGain, double &targetY)
> > +template<typename T>
> > +std::ostream &operator<<(std::ostream &os, const std::vector<T> &v)
> >  {
> > -     targetY = c.yTarget.eval(c.yTarget.domain().clip(lux));
> > -     targetY = std::min(EvGainYTargetLimit, targetY * evGain);
> > -     double iqm = h.interQuantileMean(c.qLo, c.qHi);
> > -     return (targetY * h.bins()) / iqm;
> > +     os << "{";
> > +     for (const auto &e : v)
> > +             os << " " << e;
> > +     os << " }";
> > +     return os;
> >  }
> >
> > -void Agc::computeGain(StatisticsPtr &statistics, Metadata *imageMetadata,
> > -                   double &gain, double &targetY)
> > +void Agc::setActiveChannels(const std::vector<unsigned int> &activeChannels)
> >  {
> > -     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) << "No lux level found";
> > -     const Histogram &h = statistics->yHist;
> > -     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(EvGainYTargetLimit, 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;
> > -             }
> > +     if (activeChannels.empty()) {
> > +             LOG(RPiAgc, Warning) << "No active AGC channels supplied";
> > +             return;
> >       }
> > -     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 = limitShutter(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;
> > -     filtered_.totalExposureNoDG = filtered_.totalExposure / dg;
> > -     LOG(RPiAgc, Debug) << "Target totalExposureNoDG " << filtered_.totalExposureNoDG;
> > -     return desaturate;
> > -}
> > -
> > -void Agc::filterExposure()
> > -{
> > -     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;
> > -     } 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);
> > -     }
> > -     LOG(RPiAgc, Debug) << "After filtering, totalExposure " << filtered_.totalExposure
> > -                        << " no dg " << filtered_.totalExposureNoDG;
> > -}
> > +     for (auto index : activeChannels)
> > +             if (checkChannel(index))
> > +                     return;
> >
> > -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 = limitShutter(shutterTime);
> > -     analogueGain = status_.fixedAnalogueGain != 0.0 ? status_.fixedAnalogueGain
> > -                                                     : exposureMode_->gain[0];
> > -     analogueGain = limitGain(analogueGain);
> > -     if (shutterTime * analogueGain < exposureValue) {
> > -             for (unsigned int stage = 1;
> > -                  stage < exposureMode_->gain.size(); stage++) {
> > -                     if (!status_.fixedShutter) {
> > -                             Duration stageShutter =
> > -                                     limitShutter(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];
> > -                             analogueGain = limitGain(analogueGain);
> > -                     }
> > -             }
> > -     }
> > -     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());
> > -                     analogueGain = limitGain(analogueGain);
> > -                     shutterTime = newShutterTime;
> > -             }
> > -             LOG(RPiAgc, Debug) << "After flicker avoidance, shutter "
> > -                                << shutterTime << " gain " << analogueGain;
> > -     }
> > -     filtered_.shutter = shutterTime;
> > -     filtered_.analogueGain = analogueGain;
> > +     LOG(RPiAgc, Debug) << "setActiveChannels " << activeChannels;
> > +     activeChannels_ = activeChannels;
> >  }
> >
> > -void Agc::writeAndFinish(Metadata *imageMetadata, bool desaturate)
> > +void Agc::switchMode(CameraMode const &cameraMode,
> > +                  Metadata *metadata)
> >  {
> > -     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;
> > +     LOG(RPiAgc, Debug) << "switchMode for channel 0";
> > +     channelData_[0].channel.switchMode(cameraMode, metadata);
> >  }
> >
> > -Duration Agc::limitShutter(Duration shutter)
> > +void Agc::prepare(Metadata *imageMetadata)
> >  {
> > -     /*
> > -      * shutter == 0 is a special case for fixed shutter values, and must pass
> > -      * through unchanged
> > -      */
> > -     if (!shutter)
> > -             return shutter;
> > -
> > -     shutter = std::clamp(shutter, mode_.minShutter, maxShutter_);
> > -     return shutter;
> > +     LOG(RPiAgc, Debug) << "prepare for channel 0";
> > +     channelData_[0].channel.prepare(imageMetadata);
> >  }
> >
> > -double Agc::limitGain(double gain) const
> > +void Agc::process(StatisticsPtr &stats, Metadata *imageMetadata)
> >  {
> > -     /*
> > -      * Only limit the lower bounds of the gain value to what the sensor limits.
> > -      * The upper bound on analogue gain will be made up with additional digital
> > -      * gain applied by the ISP.
> > -      *
> > -      * gain == 0.0 is a special case for fixed shutter values, and must pass
> > -      * through unchanged
> > -      */
> > -     if (!gain)
> > -             return gain;
> > -
> > -     gain = std::max(gain, mode_.minAnalogueGain);
> > -     return gain;
> > +     LOG(RPiAgc, Debug) << "process for channel 0";
> > +     channelData_[0].channel.process(stats, imageMetadata);
> >  }
> >
> >  /* Register algorithm with the system. */
> > diff --git a/src/ipa/rpi/controller/rpi/agc.h b/src/ipa/rpi/controller/rpi/agc.h
> > index aaf77c8f..24f0a271 100644
> > --- a/src/ipa/rpi/controller/rpi/agc.h
> > +++ b/src/ipa/rpi/controller/rpi/agc.h
> > @@ -6,60 +6,18 @@
> >   */
> >  #pragma once
> >
> > +#include <optional>
>
> Is this used ?

You're right, having moved those data fields into the next commit,
this include can move too!

>
> > +#include <string>
> >  #include <vector>
> > -#include <mutex>
> > -
> > -#include <libcamera/base/utils.h>
> >
> >  #include "../agc_algorithm.h"
> > -#include "../agc_status.h"
> > -#include "../pwl.h"
> >
> > -/* This is our implementation of AGC. */
> > +#include "agc_channel.h"
> >
> >  namespace RPiController {
> >
> > -struct AgcMeteringMode {
> > -     std::vector<double> weights;
> > -     int read(const libcamera::YamlObject &params);
> > -};
> > -
> > -struct AgcExposureMode {
> > -     std::vector<libcamera::utils::Duration> shutter;
> > -     std::vector<double> gain;
> > -     int read(const libcamera::YamlObject &params);
> > -};
> > -
> > -struct AgcConstraint {
> > -     enum class Bound { LOWER = 0, UPPER = 1 };
> > -     Bound bound;
> > -     double qLo;
> > -     double qHi;
> > -     Pwl yTarget;
> > -     int read(const libcamera::YamlObject &params);
> > -};
> > -
> > -typedef std::vector<AgcConstraint> AgcConstraintMode;
> > -
> > -struct AgcConfig {
> > -     int read(const libcamera::YamlObject &params);
> > -     std::map<std::string, AgcMeteringMode> meteringModes;
> > -     std::map<std::string, AgcExposureMode> exposureModes;
> > -     std::map<std::string, AgcConstraintMode> constraintModes;
> > -     Pwl yTarget;
> > -     double speed;
> > -     uint16_t startupFrames;
> > -     unsigned int convergenceFrames;
> > -     double maxChange;
> > -     double minChange;
> > -     double fastReduceThreshold;
> > -     double speedUpThreshold;
> > -     std::string defaultMeteringMode;
> > -     std::string defaultExposureMode;
> > -     std::string defaultConstraintMode;
> > -     double baseEv;
> > -     libcamera::utils::Duration defaultExposureTime;
> > -     double defaultAnalogueGain;
> > +struct AgcChannelData {
> > +     AgcChannel channel;
> >  };
> >
> >  class Agc : public AgcAlgorithm
> > @@ -70,65 +28,30 @@ public:
> >       int read(const libcamera::YamlObject &params) override;
> >       unsigned int getConvergenceFrames() const override;
> >       std::vector<double> const &getWeights() const override;
> > -     void setEv(double ev) override;
> > -     void setFlickerPeriod(libcamera::utils::Duration flickerPeriod) override;
> > +     void setEv(unsigned int channel, double ev) override;
> > +     void setFlickerPeriod(unsigned int channelIndex,
> > +                           libcamera::utils::Duration flickerPeriod) override;
> >       void setMaxShutter(libcamera::utils::Duration maxShutter) override;
> > -     void setFixedShutter(libcamera::utils::Duration fixedShutter) override;
> > -     void setFixedAnalogueGain(double fixedAnalogueGain) override;
> > +     void setFixedShutter(unsigned int channelIndex,
> > +                          libcamera::utils::Duration fixedShutter) override;
> > +     void setFixedAnalogueGain(unsigned int channelIndex,
> > +                               double fixedAnalogueGain) override;
> >       void setMeteringMode(std::string const &meteringModeName) override;
> > -     void setExposureMode(std::string const &exposureModeName) override;
> > -     void setConstraintMode(std::string const &contraintModeName) override;
> > -     void enableAuto() override;
> > -     void disableAuto() override;
> > +     void setExposureMode(unsigned int channelIndex,
> > +                          std::string const &exposureModeName) override;
> > +     void setConstraintMode(unsigned int channelIndex,
> > +                            std::string const &contraintModeName) override;
> > +     void enableAuto(unsigned int channelIndex) override;
> > +     void disableAuto(unsigned int channelIndex) override;
> >       void switchMode(CameraMode const &cameraMode, Metadata *metadata) override;
> >       void prepare(Metadata *imageMetadata) override;
> >       void process(StatisticsPtr &stats, Metadata *imageMetadata) override;
> > +     void setActiveChannels(const std::vector<unsigned int> &activeChannels) override;
> >
> >  private:
> > -     bool updateLockStatus(DeviceStatus const &deviceStatus);
> > -     AgcConfig config_;
> > -     void housekeepConfig();
> > -     void fetchCurrentExposure(Metadata *imageMetadata);
> > -     void fetchAwbStatus(Metadata *imageMetadata);
> > -     void computeGain(StatisticsPtr &statistics, Metadata *imageMetadata,
> > -                      double &gain, double &targetY);
> > -     void computeTargetExposure(double gain);
> > -     void filterExposure();
> > -     bool applyDigitalGain(double gain, double targetY);
> > -     void divideUpExposure();
> > -     void writeAndFinish(Metadata *imageMetadata, bool desaturate);
> > -     libcamera::utils::Duration limitShutter(libcamera::utils::Duration shutter);
> > -     double limitGain(double gain) const;
> > -     AgcMeteringMode *meteringMode_;
> > -     AgcExposureMode *exposureMode_;
> > -     AgcConstraintMode *constraintMode_;
> > -     CameraMode mode_;
> > -     uint64_t frameCount_;
> > -     AwbStatus awb_;
> > -     struct ExposureValues {
> > -             ExposureValues();
> > -
> > -             libcamera::utils::Duration shutter;
> > -             double analogueGain;
> > -             libcamera::utils::Duration totalExposure;
> > -             libcamera::utils::Duration totalExposureNoDG; /* without digital gain */
> > -     };
> > -     ExposureValues current_;  /* values for the current frame */
> > -     ExposureValues target_;   /* calculate the values we want here */
> > -     ExposureValues filtered_; /* these values are filtered towards target */
> > -     AgcStatus status_;
> > -     int lockCount_;
> > -     DeviceStatus lastDeviceStatus_;
> > -     libcamera::utils::Duration lastTargetExposure_;
> > -     /* Below here the "settings" that applications can change. */
> > -     std::string meteringModeName_;
> > -     std::string exposureModeName_;
> > -     std::string constraintModeName_;
> > -     double ev_;
> > -     libcamera::utils::Duration flickerPeriod_;
> > -     libcamera::utils::Duration maxShutter_;
> > -     libcamera::utils::Duration fixedShutter_;
> > -     double fixedAnalogueGain_;
> > +     int checkChannel(unsigned int channel) const;
> > +     std::vector<AgcChannelData> channelData_;
> > +     std::vector<unsigned int> activeChannels_;
> >  };
> >
> >  } /* namespace RPiController */
> > diff --git a/src/ipa/rpi/controller/rpi/agc_channel.cpp b/src/ipa/rpi/controller/rpi/agc_channel.cpp
> > new file mode 100644
> > index 00000000..7c1aba81
> > --- /dev/null
> > +++ b/src/ipa/rpi/controller/rpi/agc_channel.cpp
> > @@ -0,0 +1,924 @@
> > +/* SPDX-License-Identifier: BSD-2-Clause */
> > +/*
> > + * Copyright (C) 2023, Raspberry Pi Ltd
> > + *
> > + * agc.cpp - AGC/AEC control algorithm
>
> agc_channel.cpp

Thank you!

>
> > + */
> > +
> > +#include "agc_channel.h"
> > +
> > +#include <algorithm>
> > +#include <tuple>
> > +
> > +#include <libcamera/base/log.h>
> > +
> > +#include "../awb_status.h"
> > +#include "../device_status.h"
> > +#include "../histogram.h"
> > +#include "../lux_status.h"
> > +#include "../metadata.h"
> > +
> > +using namespace RPiController;
> > +using namespace libcamera;
> > +using libcamera::utils::Duration;
> > +using namespace std::literals::chrono_literals;
> > +
> > +LOG_DECLARE_CATEGORY(RPiAgc)
> > +
> > +int AgcMeteringMode::read(const libcamera::YamlObject &params)
> > +{
> > +     const YamlObject &yamlWeights = params["weights"];
> > +
> > +     for (const auto &p : yamlWeights.asList()) {
> > +             auto value = p.get<double>();
> > +             if (!value)
> > +                     return -EINVAL;
> > +             weights.push_back(*value);
> > +     }
> > +
> > +     return 0;
> > +}
> > +
> > +static std::tuple<int, std::string>
> > +readMeteringModes(std::map<std::string, AgcMeteringMode> &metering_modes,
> > +               const libcamera::YamlObject &params)
> > +{
> > +     std::string first;
> > +     int ret;
> > +
> > +     for (const auto &[key, value] : params.asDict()) {
> > +             AgcMeteringMode meteringMode;
> > +             ret = meteringMode.read(value);
> > +             if (ret)
> > +                     return { ret, {} };
> > +
> > +             metering_modes[key] = std::move(meteringMode);
> > +             if (first.empty())
> > +                     first = key;
> > +     }
> > +
> > +     return { 0, first };
> > +}
> > +
> > +int AgcExposureMode::read(const libcamera::YamlObject &params)
> > +{
> > +     auto value = params["shutter"].getList<double>();
> > +     if (!value)
> > +             return -EINVAL;
> > +     std::transform(value->begin(), value->end(), std::back_inserter(shutter),
> > +                    [](double v) { return v * 1us; });
> > +
> > +     value = params["gain"].getList<double>();
> > +     if (!value)
> > +             return -EINVAL;
> > +     gain = std::move(*value);
> > +
> > +     if (shutter.size() < 2 || gain.size() < 2) {
> > +             LOG(RPiAgc, Error)
> > +                     << "AgcExposureMode: must have at least two entries in exposure profile";
> > +             return -EINVAL;
> > +     }
> > +
> > +     if (shutter.size() != gain.size()) {
> > +             LOG(RPiAgc, Error)
> > +                     << "AgcExposureMode: expect same number of exposure and gain entries in exposure profile";
> > +             return -EINVAL;
> > +     }
> > +
> > +     return 0;
> > +}
> > +
> > +static std::tuple<int, std::string>
> > +readExposureModes(std::map<std::string, AgcExposureMode> &exposureModes,
> > +               const libcamera::YamlObject &params)
> > +{
> > +     std::string first;
> > +     int ret;
> > +
> > +     for (const auto &[key, value] : params.asDict()) {
> > +             AgcExposureMode exposureMode;
> > +             ret = exposureMode.read(value);
> > +             if (ret)
> > +                     return { ret, {} };
> > +
> > +             exposureModes[key] = std::move(exposureMode);
> > +             if (first.empty())
> > +                     first = key;
> > +     }
> > +
> > +     return { 0, first };
> > +}
> > +
> > +int AgcConstraint::read(const libcamera::YamlObject &params)
> > +{
> > +     std::string boundString = params["bound"].get<std::string>("");
> > +     transform(boundString.begin(), boundString.end(),
> > +               boundString.begin(), ::toupper);
> > +     if (boundString != "UPPER" && boundString != "LOWER") {
> > +             LOG(RPiAgc, Error) << "AGC constraint type should be UPPER or LOWER";
> > +             return -EINVAL;
> > +     }
> > +     bound = boundString == "UPPER" ? Bound::UPPER : Bound::LOWER;
> > +
> > +     auto value = params["q_lo"].get<double>();
> > +     if (!value)
> > +             return -EINVAL;
> > +     qLo = *value;
> > +
> > +     value = params["q_hi"].get<double>();
> > +     if (!value)
> > +             return -EINVAL;
> > +     qHi = *value;
> > +
> > +     return yTarget.read(params["y_target"]);
> > +}
> > +
> > +static std::tuple<int, AgcConstraintMode>
> > +readConstraintMode(const libcamera::YamlObject &params)
> > +{
> > +     AgcConstraintMode mode;
> > +     int ret;
> > +
> > +     for (const auto &p : params.asList()) {
> > +             AgcConstraint constraint;
> > +             ret = constraint.read(p);
> > +             if (ret)
> > +                     return { ret, {} };
> > +
> > +             mode.push_back(std::move(constraint));
> > +     }
> > +
> > +     return { 0, mode };
> > +}
> > +
> > +static std::tuple<int, std::string>
> > +readConstraintModes(std::map<std::string, AgcConstraintMode> &constraintModes,
> > +                 const libcamera::YamlObject &params)
> > +{
> > +     std::string first;
> > +     int ret;
> > +
> > +     for (const auto &[key, value] : params.asDict()) {
> > +             std::tie(ret, constraintModes[key]) = readConstraintMode(value);
> > +             if (ret)
> > +                     return { ret, {} };
> > +
> > +             if (first.empty())
> > +                     first = key;
> > +     }
> > +
> > +     return { 0, first };
> > +}
> > +
> > +int AgcConfig::read(const libcamera::YamlObject &params)
> > +{
> > +     LOG(RPiAgc, Debug) << "AgcConfig";
> > +     int ret;
> > +
> > +     std::tie(ret, defaultMeteringMode) =
> > +             readMeteringModes(meteringModes, params["metering_modes"]);
> > +     if (ret)
> > +             return ret;
> > +     std::tie(ret, defaultExposureMode) =
> > +             readExposureModes(exposureModes, params["exposure_modes"]);
> > +     if (ret)
> > +             return ret;
> > +     std::tie(ret, defaultConstraintMode) =
> > +             readConstraintModes(constraintModes, params["constraint_modes"]);
> > +     if (ret)
> > +             return ret;
> > +
> > +     ret = yTarget.read(params["y_target"]);
> > +     if (ret)
> > +             return ret;
> > +
> > +     speed = params["speed"].get<double>(0.2);
> > +     startupFrames = params["startup_frames"].get<uint16_t>(10);
> > +     convergenceFrames = params["convergence_frames"].get<unsigned int>(6);
> > +     fastReduceThreshold = params["fast_reduce_threshold"].get<double>(0.4);
> > +     baseEv = params["base_ev"].get<double>(1.0);
> > +
> > +     /* Start with quite a low value as ramping up is easier than ramping down. */
> > +     defaultExposureTime = params["default_exposure_time"].get<double>(1000) * 1us;
> > +     defaultAnalogueGain = params["default_analogue_gain"].get<double>(1.0);
> > +
> > +     return 0;
> > +}
> > +
> > +AgcChannel::ExposureValues::ExposureValues()
> > +     : shutter(0s), analogueGain(0),
> > +       totalExposure(0s), totalExposureNoDG(0s)
> > +{
> > +}
> > +
> > +AgcChannel::AgcChannel()
> > +     : meteringMode_(nullptr), exposureMode_(nullptr), constraintMode_(nullptr),
> > +       frameCount_(0), lockCount_(0),
> > +       lastTargetExposure_(0s), 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).
> > +      */
> > +     status_ = {};
> > +     status_.ev = ev_;
> > +}
> > +
> > +int AgcChannel::read(const libcamera::YamlObject &params,
> > +                  const Controller::HardwareConfig &hardwareConfig)
> > +{
> > +     int ret = config_.read(params);
> > +     if (ret)
> > +             return ret;
> > +
> > +     const Size &size = hardwareConfig.agcZoneWeights;
> > +     for (auto const &modes : config_.meteringModes) {
> > +             if (modes.second.weights.size() != size.width * size.height) {
> > +                     LOG(RPiAgc, Error) << "AgcMeteringMode: Incorrect number of weights";
> > +                     return -EINVAL;
> > +             }
> > +     }
> > +
> > +     /*
> > +      * 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;
> > +     return 0;
> > +}
> > +
> > +void AgcChannel::disableAuto()
> > +{
> > +     fixedShutter_ = status_.shutterTime;
> > +     fixedAnalogueGain_ = status_.analogueGain;
> > +}
> > +
> > +void AgcChannel::enableAuto()
> > +{
> > +     fixedShutter_ = 0s;
> > +     fixedAnalogueGain_ = 0;
> > +}
> > +
> > +unsigned int AgcChannel::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;
> > +}
> > +
> > +std::vector<double> const &AgcChannel::getWeights() const
> > +{
> > +     /*
> > +      * In case someone calls setMeteringMode and then this before the
> > +      * algorithm has run and updated the meteringMode_ pointer.
> > +      */
> > +     auto it = config_.meteringModes.find(meteringModeName_);
> > +     if (it == config_.meteringModes.end())
> > +             return meteringMode_->weights;
> > +     return it->second.weights;
> > +}
> > +
> > +void AgcChannel::setEv(double ev)
> > +{
> > +     ev_ = ev;
> > +}
> > +
> > +void AgcChannel::setFlickerPeriod(Duration flickerPeriod)
> > +{
> > +     flickerPeriod_ = flickerPeriod;
> > +}
> > +
> > +void AgcChannel::setMaxShutter(Duration maxShutter)
> > +{
> > +     maxShutter_ = maxShutter;
> > +}
> > +
> > +void AgcChannel::setFixedShutter(Duration fixedShutter)
> > +{
> > +     fixedShutter_ = fixedShutter;
> > +     /* Set this in case someone calls disableAuto() straight after. */
> > +     status_.shutterTime = limitShutter(fixedShutter_);
> > +}
> > +
> > +void AgcChannel::setFixedAnalogueGain(double fixedAnalogueGain)
> > +{
> > +     fixedAnalogueGain_ = fixedAnalogueGain;
> > +     /* Set this in case someone calls disableAuto() straight after. */
> > +     status_.analogueGain = limitGain(fixedAnalogueGain);
> > +}
> > +
> > +void AgcChannel::setMeteringMode(std::string const &meteringModeName)
> > +{
> > +     meteringModeName_ = meteringModeName;
> > +}
> > +
> > +void AgcChannel::setExposureMode(std::string const &exposureModeName)
> > +{
> > +     exposureModeName_ = exposureModeName;
> > +}
> > +
> > +void AgcChannel::setConstraintMode(std::string const &constraintModeName)
> > +{
> > +     constraintModeName_ = constraintModeName;
> > +}
> > +
> > +void AgcChannel::switchMode(CameraMode const &cameraMode,
> > +                         Metadata *metadata)
> > +{
> > +     /* AGC expects the mode sensitivity always to be non-zero. */
> > +     ASSERT(cameraMode.sensitivity);
> > +
> > +     housekeepConfig();
> > +
> > +     /*
> > +      * Store the mode in the local state. We must cache the sensitivity of
> > +      * of the previous mode for the calculations below.
> > +      */
> > +     double lastSensitivity = mode_.sensitivity;
> > +     mode_ = cameraMode;
> > +
> > +     Duration fixedShutter = limitShutter(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);
> > +}
> > +
> > +void AgcChannel::prepare(Metadata *imageMetadata)
> > +{
> > +     Duration totalExposureValue = status_.totalExposureValue;
> > +     AgcStatus delayedStatus;
> > +     AgcPrepareStatus prepareStatus;
> > +
> > +     if (!imageMetadata->get("agc.delayed_status", delayedStatus))
> > +             totalExposureValue = delayedStatus.totalExposureValue;
> > +
> > +     prepareStatus.digitalGain = 1.0;
> > +     prepareStatus.locked = false;
> > +
> > +     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) {
> > +                             double digitalGain = totalExposureValue / actualExposure;
> > +                             LOG(RPiAgc, Debug) << "Want total exposure " << totalExposureValue;
> > +                             /*
> > +                              * Never ask for a gain < 1.0, and also impose
> > +                              * some upper limit. Make it customisable?
> > +                              */
> > +                             prepareStatus.digitalGain = std::max(1.0, std::min(digitalGain, 4.0));
> > +                             LOG(RPiAgc, Debug) << "Actual exposure " << actualExposure;
> > +                             LOG(RPiAgc, Debug) << "Use digitalGain " << prepareStatus.digitalGain;
> > +                             LOG(RPiAgc, Debug) << "Effective exposure "
> > +                                                << actualExposure * prepareStatus.digitalGain;
> > +                             /* Decide whether AEC/AGC has converged. */
> > +                             prepareStatus.locked = updateLockStatus(deviceStatus);
> > +                     }
> > +             } else
> > +                     LOG(RPiAgc, Warning) << "AgcChannel: no device metadata";
> > +             imageMetadata->set("agc.prepare_status", prepareStatus);
> > +     }
> > +}
> > +
> > +void AgcChannel::process(StatisticsPtr &stats, Metadata *imageMetadata)
> > +{
> > +     frameCount_++;
> > +     /*
> > +      * First a little bit of housekeeping, fetching up-to-date settings and
> > +      * configuration, that kind of thing.
> > +      */
> > +     housekeepConfig();
> > +     /* Fetch the AWB status immediately, so that we can assume it's there. */
> > +     fetchAwbStatus(imageMetadata);
> > +     /* 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, imageMetadata, gain, targetY);
> > +     /* Now compute the target (final) exposure which we think we want. */
> > +     computeTargetExposure(gain);
> > +     /* The results have to be filtered so as not to change too rapidly. */
> > +     filterExposure();
> > +     /*
> > +      * 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 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);
> > +}
> > +
> > +bool AgcChannel::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_;
> > +     return lockCount_ == maxLockCount;
> > +}
> > +
> > +void AgcChannel::housekeepConfig()
> > +{
> > +     /* First fetch all the up-to-date settings, so no one else has to do it. */
> > +     status_.ev = ev_;
> > +     status_.fixedShutter = limitShutter(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 (meteringModeName_ != status_.meteringMode) {
> > +             auto it = config_.meteringModes.find(meteringModeName_);
> > +             if (it == config_.meteringModes.end()) {
> > +                     LOG(RPiAgc, Warning) << "No metering mode " << meteringModeName_;
> > +                     meteringModeName_ = status_.meteringMode;
> > +             } else {
> > +                     meteringMode_ = &it->second;
> > +                     status_.meteringMode = meteringModeName_;
> > +             }
> > +     }
> > +     if (exposureModeName_ != status_.exposureMode) {
> > +             auto it = config_.exposureModes.find(exposureModeName_);
> > +             if (it == config_.exposureModes.end()) {
> > +                     LOG(RPiAgc, Warning) << "No exposure profile " << exposureModeName_;
> > +                     exposureModeName_ = status_.exposureMode;
> > +             } else {
> > +                     exposureMode_ = &it->second;
> > +                     status_.exposureMode = exposureModeName_;
> > +             }
> > +     }
> > +     if (constraintModeName_ != status_.constraintMode) {
> > +             auto it = config_.constraintModes.find(constraintModeName_);
> > +             if (it == config_.constraintModes.end()) {
> > +                     LOG(RPiAgc, Warning) << "No constraint list " << constraintModeName_;
> > +                     constraintModeName_ = status_.constraintMode;
> > +             } else {
> > +                     constraintMode_ = &it->second;
> > +                     status_.constraintMode = constraintModeName_;
> > +             }
> > +     }
> > +     LOG(RPiAgc, Debug) << "exposureMode "
> > +                        << exposureModeName_ << " constraintMode "
> > +                        << constraintModeName_ << " meteringMode "
> > +                        << meteringModeName_;
> > +}
> > +
> > +void AgcChannel::fetchCurrentExposure(Metadata *imageMetadata)
> > +{
> > +     std::unique_lock<Metadata> lock(*imageMetadata);
> > +     DeviceStatus *deviceStatus =
> > +             imageMetadata->getLocked<DeviceStatus>("device.status");
> > +     if (!deviceStatus)
> > +             LOG(RPiAgc, Fatal) << "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 AgcChannel::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) << "No AWB status found";
> > +}
> > +
> > +static double computeInitialY(StatisticsPtr &stats, AwbStatus const &awb,
> > +                           std::vector<double> &weights, double gain)
> > +{
> > +     constexpr uint64_t maxVal = 1 << Statistics::NormalisationFactorPow2;
> > +
> > +     /*
> > +      * If we have no AGC region stats, but do have a a Y histogram, use that
> > +      * directly to caluclate the mean Y value of the image.
> > +      */
> > +     if (!stats->agcRegions.numRegions() && stats->yHist.bins()) {
> > +             /*
> > +              * When the gain is applied to the histogram, anything below minBin
> > +              * will scale up directly with the gain, but anything above that
> > +              * will saturate into the top bin.
> > +              */
> > +             auto &hist = stats->yHist;
> > +             double minBin = std::min(1.0, 1.0 / gain) * hist.bins();
> > +             double binMean = hist.interBinMean(0.0, minBin);
> > +             double numUnsaturated = hist.cumulativeFreq(minBin);
> > +             /* This term is from all the pixels that won't saturate. */
> > +             double ySum = binMean * gain * numUnsaturated;
> > +             /* And add the ones that will saturate. */
> > +             ySum += (hist.total() - numUnsaturated) * hist.bins();
> > +             return ySum / hist.total() / hist.bins();
> > +     }
> > +
> > +     ASSERT(weights.size() == stats->agcRegions.numRegions());
> > +
> > +     /*
> > +      * Note that the weights are applied by the IPA to the statistics directly,
> > +      * before they are given to us here.
> > +      */
> > +     double rSum = 0, gSum = 0, bSum = 0, pixelSum = 0;
> > +     for (unsigned int i = 0; i < stats->agcRegions.numRegions(); i++) {
> > +             auto &region = stats->agcRegions.get(i);
> > +             rSum += std::min<double>(region.val.rSum * gain, (maxVal - 1) * region.counted);
> > +             gSum += std::min<double>(region.val.gSum * gain, (maxVal - 1) * region.counted);
> > +             bSum += std::min<double>(region.val.bSum * gain, (maxVal - 1) * region.counted);
> > +             pixelSum += region.counted;
> > +     }
> > +     if (pixelSum == 0.0) {
> > +             LOG(RPiAgc, Warning) << "computeInitialY: pixelSum is zero";
> > +             return 0;
> > +     }
> > +
> > +     double ySum;
> > +     /* Factor in the AWB correction if needed. */
> > +     if (stats->agcStatsPos == Statistics::AgcStatsPos::PreWb) {
> > +             ySum = rSum * awb.gainR * .299 +
> > +                    gSum * awb.gainG * .587 +
> > +                    gSum * awb.gainB * .114;
> > +     } else
> > +             ySum = rSum * .299 + gSum * .587 + gSum * .114;
> > +
> > +     return ySum / pixelSum / (1 << 16);
> > +}
> > +
> > +/*
> > + * 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.
> > + */
> > +
> > +static constexpr double EvGainYTargetLimit = 0.9;
> > +
> > +static double constraintComputeGain(AgcConstraint &c, const Histogram &h, double lux,
> > +                                 double evGain, double &targetY)
> > +{
> > +     targetY = c.yTarget.eval(c.yTarget.domain().clip(lux));
> > +     targetY = std::min(EvGainYTargetLimit, targetY * evGain);
> > +     double iqm = h.interQuantileMean(c.qLo, c.qHi);
> > +     return (targetY * h.bins()) / iqm;
> > +}
> > +
> > +void AgcChannel::computeGain(StatisticsPtr &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) << "No lux level found";
> > +     const Histogram &h = statistics->yHist;
> > +     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(EvGainYTargetLimit, 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 AgcChannel::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 = limitShutter(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 AgcChannel::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;
> > +     filtered_.totalExposureNoDG = filtered_.totalExposure / dg;
> > +     LOG(RPiAgc, Debug) << "Target totalExposureNoDG " << filtered_.totalExposureNoDG;
> > +     return desaturate;
> > +}
> > +
> > +void AgcChannel::filterExposure()
> > +{
> > +     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;
> > +     } 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);
> > +     }
> > +     LOG(RPiAgc, Debug) << "After filtering, totalExposure " << filtered_.totalExposure
> > +                        << " no dg " << filtered_.totalExposureNoDG;
> > +}
> > +
> > +void AgcChannel::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 = limitShutter(shutterTime);
> > +     analogueGain = status_.fixedAnalogueGain != 0.0 ? status_.fixedAnalogueGain
> > +                                                     : exposureMode_->gain[0];
> > +     analogueGain = limitGain(analogueGain);
> > +     if (shutterTime * analogueGain < exposureValue) {
> > +             for (unsigned int stage = 1;
> > +                  stage < exposureMode_->gain.size(); stage++) {
> > +                     if (!status_.fixedShutter) {
> > +                             Duration stageShutter =
> > +                                     limitShutter(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];
> > +                             analogueGain = limitGain(analogueGain);
> > +                     }
> > +             }
> > +     }
> > +     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());
> > +                     analogueGain = limitGain(analogueGain);
> > +                     shutterTime = newShutterTime;
> > +             }
> > +             LOG(RPiAgc, Debug) << "After flicker avoidance, shutter "
> > +                                << shutterTime << " gain " << analogueGain;
> > +     }
> > +     filtered_.shutter = shutterTime;
> > +     filtered_.analogueGain = analogueGain;
> > +}
> > +
> > +void AgcChannel::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 AgcChannel::limitShutter(Duration shutter)
> > +{
> > +     /*
> > +      * shutter == 0 is a special case for fixed shutter values, and must pass
> > +      * through unchanged
> > +      */
> > +     if (!shutter)
> > +             return shutter;
> > +
> > +     shutter = std::clamp(shutter, mode_.minShutter, maxShutter_);
> > +     return shutter;
> > +}
> > +
> > +double AgcChannel::limitGain(double gain) const
> > +{
> > +     /*
> > +      * Only limit the lower bounds of the gain value to what the sensor limits.
> > +      * The upper bound on analogue gain will be made up with additional digital
> > +      * gain applied by the ISP.
> > +      *
> > +      * gain == 0.0 is a special case for fixed shutter values, and must pass
> > +      * through unchanged
> > +      */
> > +     if (!gain)
> > +             return gain;
> > +
> > +     gain = std::max(gain, mode_.minAnalogueGain);
> > +     return gain;
> > +}
> > diff --git a/src/ipa/rpi/controller/rpi/agc_channel.h b/src/ipa/rpi/controller/rpi/agc_channel.h
> > new file mode 100644
> > index 00000000..d5a5cf3a
> > --- /dev/null
> > +++ b/src/ipa/rpi/controller/rpi/agc_channel.h
> > @@ -0,0 +1,137 @@
> > +/* SPDX-License-Identifier: BSD-2-Clause */
> > +/*
> > + * Copyright (C) 2023, Raspberry Pi Ltd
> > + *
> > + * agc.h - AGC/AEC control algorithm
>
> And agc_channel.h

And again.

Thanks for doing all this reviewing!

Best regards
David

>
> Sorry for missing these in the previous review
>
> The rest has been clarified in the previous review and the code is
> just mostly moved around
>
> Reviewed-by: Jacopo Mondi <jacopo.mondi at ideasonboard.com>
>
> Thanks
>   j
>
>
> > + */
> > +#pragma once
> > +
> > +#include <map>
> > +#include <string>
> > +#include <vector>
> > +
> > +#include <libcamera/base/utils.h>
> > +
> > +#include "../agc_status.h"
> > +#include "../awb_status.h"
> > +#include "../controller.h"
> > +#include "../pwl.h"
> > +
> > +/* This is our implementation of AGC. */
> > +
> > +namespace RPiController {
> > +
> > +struct AgcMeteringMode {
> > +     std::vector<double> weights;
> > +     int read(const libcamera::YamlObject &params);
> > +};
> > +
> > +struct AgcExposureMode {
> > +     std::vector<libcamera::utils::Duration> shutter;
> > +     std::vector<double> gain;
> > +     int read(const libcamera::YamlObject &params);
> > +};
> > +
> > +struct AgcConstraint {
> > +     enum class Bound { LOWER = 0,
> > +                        UPPER = 1 };
> > +     Bound bound;
> > +     double qLo;
> > +     double qHi;
> > +     Pwl yTarget;
> > +     int read(const libcamera::YamlObject &params);
> > +};
> > +
> > +typedef std::vector<AgcConstraint> AgcConstraintMode;
> > +
> > +struct AgcConfig {
> > +     int read(const libcamera::YamlObject &params);
> > +     std::map<std::string, AgcMeteringMode> meteringModes;
> > +     std::map<std::string, AgcExposureMode> exposureModes;
> > +     std::map<std::string, AgcConstraintMode> constraintModes;
> > +     Pwl yTarget;
> > +     double speed;
> > +     uint16_t startupFrames;
> > +     unsigned int convergenceFrames;
> > +     double maxChange;
> > +     double minChange;
> > +     double fastReduceThreshold;
> > +     double speedUpThreshold;
> > +     std::string defaultMeteringMode;
> > +     std::string defaultExposureMode;
> > +     std::string defaultConstraintMode;
> > +     double baseEv;
> > +     libcamera::utils::Duration defaultExposureTime;
> > +     double defaultAnalogueGain;
> > +};
> > +
> > +class AgcChannel
> > +{
> > +public:
> > +     AgcChannel();
> > +     int read(const libcamera::YamlObject &params,
> > +              const Controller::HardwareConfig &hardwareConfig);
> > +     unsigned int getConvergenceFrames() const;
> > +     std::vector<double> const &getWeights() const;
> > +     void setEv(double ev);
> > +     void setFlickerPeriod(libcamera::utils::Duration flickerPeriod);
> > +     void setMaxShutter(libcamera::utils::Duration maxShutter);
> > +     void setFixedShutter(libcamera::utils::Duration fixedShutter);
> > +     void setFixedAnalogueGain(double fixedAnalogueGain);
> > +     void setMeteringMode(std::string const &meteringModeName);
> > +     void setExposureMode(std::string const &exposureModeName);
> > +     void setConstraintMode(std::string const &contraintModeName);
> > +     void enableAuto();
> > +     void disableAuto();
> > +     void switchMode(CameraMode const &cameraMode, Metadata *metadata);
> > +     void prepare(Metadata *imageMetadata);
> > +     void process(StatisticsPtr &stats, Metadata *imageMetadata);
> > +
> > +private:
> > +     bool updateLockStatus(DeviceStatus const &deviceStatus);
> > +     AgcConfig config_;
> > +     void housekeepConfig();
> > +     void fetchCurrentExposure(Metadata *imageMetadata);
> > +     void fetchAwbStatus(Metadata *imageMetadata);
> > +     void computeGain(StatisticsPtr &statistics, Metadata *imageMetadata,
> > +                      double &gain, double &targetY);
> > +     void computeTargetExposure(double gain);
> > +     void filterExposure();
> > +     bool applyDigitalGain(double gain, double targetY);
> > +     void divideUpExposure();
> > +     void writeAndFinish(Metadata *imageMetadata, bool desaturate);
> > +     libcamera::utils::Duration limitShutter(libcamera::utils::Duration shutter);
> > +     double limitGain(double gain) const;
> > +     AgcMeteringMode *meteringMode_;
> > +     AgcExposureMode *exposureMode_;
> > +     AgcConstraintMode *constraintMode_;
> > +     CameraMode mode_;
> > +     uint64_t frameCount_;
> > +     AwbStatus awb_;
> > +     struct ExposureValues {
> > +             ExposureValues();
> > +
> > +             libcamera::utils::Duration shutter;
> > +             double analogueGain;
> > +             libcamera::utils::Duration totalExposure;
> > +             libcamera::utils::Duration totalExposureNoDG; /* without digital gain */
> > +     };
> > +     ExposureValues current_; /* values for the current frame */
> > +     ExposureValues target_; /* calculate the values we want here */
> > +     ExposureValues filtered_; /* these values are filtered towards target */
> > +     AgcStatus status_;
> > +     int lockCount_;
> > +     DeviceStatus lastDeviceStatus_;
> > +     libcamera::utils::Duration lastTargetExposure_;
> > +     /* Below here the "settings" that applications can change. */
> > +     std::string meteringModeName_;
> > +     std::string exposureModeName_;
> > +     std::string constraintModeName_;
> > +     double ev_;
> > +     libcamera::utils::Duration flickerPeriod_;
> > +     libcamera::utils::Duration maxShutter_;
> > +     libcamera::utils::Duration fixedShutter_;
> > +     double fixedAnalogueGain_;
> > +};
> > +
> > +} /* namespace RPiController */
> > --
> > 2.30.2
> >


More information about the libcamera-devel mailing list