/* SPDX-License-Identifier: LGPL-2.1-or-later */ /* * Copyright (C) 2023, Linaro Ltd * * soft_simple.cpp - Simple Software Image Processing Algorithm module */ #include <sys/mman.h> #include <linux/v4l2-controls.h> #include <libcamera/base/file.h> #include <libcamera/base/log.h> #include <libcamera/base/shared_fd.h> #include <libcamera/control_ids.h> #include <libcamera/controls.h> #include <libcamera/ipa/ipa_interface.h> #include <libcamera/ipa/ipa_module_info.h> #include <libcamera/ipa/soft_ipa_interface.h> #include "libcamera/internal/software_isp/debayer_params.h" #include "libcamera/internal/software_isp/swisp_stats.h" #include "libcamera/internal/yaml_parser.h" #include "libipa/camera_sensor_helper.h" #include "black_level.h" namespace libcamera { LOG_DEFINE_CATEGORY(IPASoft) namespace ipa::soft { /* * The number of bins to use for the optimal exposure calculations. */ static constexpr unsigned int kExposureBinsCount = 5; /* * The exposure is optimal when the mean sample value of the histogram is * in the middle of the range. */ static constexpr float kExposureOptimal = kExposureBinsCount / 2.0; /* * The below value implements the hysteresis for the exposure adjustment. * It is small enough to have the exposure close to the optimal, and is big * enough to prevent the exposure from wobbling around the optimal value. */ static constexpr float kExposureSatisfactory = 0.2; class IPASoftSimple : public ipa::soft::IPASoftInterface { public: IPASoftSimple() : params_(nullptr), stats_(nullptr), blackLevel_(BlackLevel()), ignoreUpdates_(0) { } ~IPASoftSimple(); int init(const IPASettings &settings, const SharedFD &fdStats, const SharedFD &fdParams, const ControlInfoMap &sensorInfoMap) override; int configure(const ControlInfoMap &sensorInfoMap) override; int start() override; void stop() override; void processStats(const ControlList &sensorControls) override; private: void updateExposure(double exposureMSV); DebayerParams *params_; SwIspStats *stats_; std::unique_ptr<CameraSensorHelper> camHelper_; ControlInfoMap sensorInfoMap_; BlackLevel blackLevel_; int32_t exposureMin_, exposureMax_; int32_t exposure_; double againMin_, againMax_, againMinStep_; double again_; unsigned int ignoreUpdates_; }; IPASoftSimple::~IPASoftSimple() { if (stats_) munmap(stats_, sizeof(SwIspStats)); if (params_) munmap(params_, sizeof(DebayerParams)); } int IPASoftSimple::init(const IPASettings &settings, const SharedFD &fdStats, const SharedFD &fdParams, const ControlInfoMap &sensorInfoMap) { camHelper_ = CameraSensorHelperFactoryBase::create(settings.sensorModel); if (!camHelper_) { LOG(IPASoft, Warning) << "Failed to create camera sensor helper for " << settings.sensorModel; } /* Load the tuning data file */ File file(settings.configurationFile); if (!file.open(File::OpenModeFlag::ReadOnly)) { int ret = file.error(); LOG(IPASoft, Error) << "Failed to open configuration file " << settings.configurationFile << ": " << strerror(-ret); return ret; } std::unique_ptr<libcamera::YamlObject> data = YamlParser::parse(file); if (!data) return -EINVAL; /* \todo Use the IPA configuration file for real. */ unsigned int version = (*data)["version"].get<uint32_t>(0); LOG(IPASoft, Debug) << "Tuning file version " << version; params_ = nullptr; stats_ = nullptr; if (!fdStats.isValid()) { LOG(IPASoft, Error) << "Invalid Statistics handle"; return -ENODEV; } if (!fdParams.isValid()) { LOG(IPASoft, Error) << "Invalid Parameters handle"; return -ENODEV; } { void *mem = mm<span class="hl kwa"><svg</span> <span class="hl kwb">xmlns</span>=<span class="hl str">"http://www.w3.org/2000/svg"</span> <span class="hl kwb">width</span>=<span class="hl str">"24"</span> <span class="hl kwb">height</span>=<span class="hl str">"24"</span> <span class="hl kwb">viewBox</span>=<span class="hl str">"0 0 24 24"</span> <span class="hl kwb">fill</span>=<span class="hl str">"none"</span> <span class="hl kwb">stroke</span>=<span class="hl str">"currentColor"</span> <span class="hl kwb">stroke-width</span>=<span class="hl str">"2"</span> <span class="hl kwb">stroke-linecap</span>=<span class="hl str">"round"</span> <span class="hl kwb">stroke-linejoin</span>=<span class="hl str">"round"</span> <span class="hl kwb">class</span>=<span class="hl str">"feather feather-shuffle"</span><span class="hl kwa">><polyline</span> <span class="hl kwb">points</span>=<span class="hl str">"16 3 21 3 21 8"</span><span class="hl kwa">></polyline><line</span> <span class="hl kwb">x1</span>=<span class="hl str">"4"</span> <span class="hl kwb">y1</span>=<span class="hl str">"20"</span> <span class="hl kwb">x2</span>=<span class="hl str">"21"</span> <span class="hl kwb">y2</span>=<span class="hl str">"3"</span><span class="hl kwa">></line><polyline</span> <span class="hl kwb">points</span>=<span class="hl str">"21 16 21 21 16 21"</span><span class="hl kwa">></polyline><line</span> <span class="hl kwb">x1</span>=<span class="hl str">"15"</span> <span class="hl kwb">y1</span>=<span class="hl str">"15"</span> <span class="hl kwb">x2</span>=<span class="hl str">"21"</span> <span class="hl kwb">y2</span>=<span class="hl str">"21"</span><span class="hl kwa">></line><line</span> <span class="hl kwb">x1</span>=<span class="hl str">"4"</span> <span class="hl kwb">y1</span>=<span class="hl str">"4"</span> <span class="hl kwb">x2</span>=<span class="hl str">"9"</span> <span class="hl kwb">y2</span>=<span class="hl str">"9"</span><span class="hl kwa">></line></svg></span> </code></pre></td></tr></table> </div> <!-- class=content --> <div class='footer'>generated by <a href='https://git.zx2c4.com/cgit/about/'>cgit v1.2.1</a> (<a href='https://git-scm.com/'>git 2.18.0</a>) at 2025-03-04 15:58:56 +0000</div> </div> <!-- id=cgit --> </body> </html> LOG(IPASoft, Info) << "Exposure " << exposureMin_ << "-" << exposureMax_ << ", gain " << againMin_ << "-" << againMax_ << " (" << againMinStep_ << ")"; return 0; } int IPASoftSimple::start() { return 0; } void IPASoftSimple::stop() { } void IPASoftSimple::processStats(const ControlList &sensorControls) { /* * Calculate red and blue gains for AWB. * Clamp max gain at 4.0, this also avoids 0 division. */ if (stats_->sumR_ <= stats_->sumG_ / 4) params_->gainR = 1024; else params_->gainR = 256 * stats_->sumG_ / stats_->sumR_; if (stats_->sumB_ <= stats_->sumG_ / 4) params_->gainB = 1024; else params_->gainB = 256 * stats_->sumG_ / stats_->sumB_; /* Green gain and gamma values are fixed */ params_->gainG = 256; params_->gamma = 0.5; if (ignoreUpdates_ > 0) blackLevel_.update(stats_->yHistogram); params_->blackLevel = blackLevel_.get(); setIspParams.emit(); /* \todo Switch to the libipa/algorithm.h API someday. */ /* * AE / AGC, use 2 frames delay to make sure that the exposure and * the gain set have applied to the camera sensor. * \todo This could be handled better with DelayedControls. */ if (ignoreUpdates_ > 0) { --ignoreUpdates_; return; } /* * Calculate Mean Sample Value (MSV) according to formula from: * https://www.araa.asn.au/acra/acra2007/papers/paper84final.pdf */ const unsigned int blackLevelHistIdx = params_->blackLevel / (256 / SwIspStats::kYHistogramSize); const unsigned int histogramSize = SwIspStats::kYHistogramSize - blackLevelHistIdx; const unsigned int yHistValsPerBin = histogramSize / kExposureBinsCount; const unsigned int yHistValsPerBinMod = histogramSize / (histogramSize % kExposureBinsCount + 1); int exposureBins[kExposureBinsCount] = {}; unsigned int denom = 0; unsigned int num = 0; for (unsigned int i = 0; i < histogramSize; i++) { unsigned int idx = (i - (i / yHistValsPerBinMod)) / yHistValsPerBin; exposureBins[idx] += stats_->yHistogram[blackLevelHistIdx + i]; } for (unsigned int i = 0; i < kExposureBinsCount; i++) { LOG(IPASoft, Debug) << i << ": " << exposureBins[i]; denom += exposureBins[i]; num += exposureBins[i] * (i + 1); } float exposureMSV = static_cast<float>(num) / denom; /* Sanity check */ if (!sensorControls.contains(V4L2_CID_EXPOSURE) || !sensorControls.contains(V4L2_CID_ANALOGUE_GAIN)) { LOG(IPASoft, Error) << "Control(s) missing"; return; } exposure_ = sensorControls.get(V4L2_CID_EXPOSURE).get<int32_t>(); int32_t again = sensorControls.get(V4L2_CID_ANALOGUE_GAIN).get<int32_t>(); again_ = camHelper_ ? camHelper_->gain(again) : again; updateExposure(exposureMSV); ControlList ctrls(sensorInfoMap_); ctrls.set(V4L2_CID_EXPOSURE, exposure_); ctrls.set(V4L2_CID_ANALOGUE_GAIN, static_cast<int32_t>(camHelper_ ? camHelper_->gainCode(again_) : again_)); ignoreUpdates_ = 2; setSensorControls.emit(ctrls); LOG(IPASoft, Debug) << "exposureMSV " << exposureMSV << " exp " << exposure_ << " again " << again_ << " gain R/B " << params_->gainR << "/" << params_->gainB << " black level " << params_->blackLevel; } void IPASoftSimple::updateExposure(double exposureMSV) { /* * kExpDenominator of 10 gives ~10% increment/decrement; * kExpDenominator of 5 - about ~20% */ static constexpr uint8_t kExpDenominator = 10; static constexpr uint8_t kExpNumeratorUp = kExpDenominator + 1; static constexpr uint8_t kExpNumeratorDown = kExpDenominator - 1; double next; if (exposureMSV < kExposureOptimal - kExposureSatisfactory) { next = exposure_ * kExpNumeratorUp / kExpDenominator; if (next - exposure_ < 1) exposure_ += 1; else exposure_ = next; if (exposure_ >= exposureMax_) { next = again_ * kExpNumeratorUp / kExpDenominator; if (next - again_ < againMinStep_) again_ += againMinStep_; else again_ = next; } } if (exposureMSV > kExposureOptimal + kExposureSatisfactory) { if (exposure_ == exposureMax_ && again_ > againMin_) { next = again_ * kExpNumeratorDown / kExpDenominator; if (again_ - next < againMinStep_) again_ -= againMinStep_; else again_ = next; } else { next = exposure_ * kExpNumeratorDown / kExpDenominator; if (exposure_ - next < 1) exposure_ -= 1; else exposure_ = next; } } exposure_ = std::clamp(exposure_, exposureMin_, exposureMax_); again_ = std::clamp(again_, againMin_, againMax_); } } /* namespace ipa::soft */ /* * External IPA module interface */ extern "C" { const struct IPAModuleInfo ipaModuleInfo = { IPA_MODULE_API_VERSION, 0, "SimplePipelineHandler", "simple", }; IPAInterface *ipaCreate() { return new ipa::soft::IPASoftSimple(); } } /* extern "C" */ } /* namespace libcamera */