#pragma once #include "fgc/ImageQuality.h" #include namespace fgc { // The exposure/gain control law: given what an image turned out like, decide // whether to keep it and what to change if not. Pure (no camera, no I/O, no // clock) so the whole correction strategy is unit-testable. struct CaptureSettings { double exposure_us = 0.0; double gain_db = 0.0; }; struct PolicyParams { double target_mean = 110.0; // desired mean luma, 0..255 double mean_tolerance = 12.0; // accept within +-this of the target double clip_max_fraction = 0.005; // 0.5% blown pixels is the ceiling double exposure_min_us = 50.0; double exposure_max_us = 20000.0; // doubles as the motion-blur budget double gain_max_db = 12.0; double damping = 0.8; // <1 damps the correction, preventing oscillation // Blur is judged RELATIVE to the same angle's own history: a foggy horizon is // legitimately low-detail, so an absolute threshold would retry forever on it. double blur_relative_floor = 0.5; // retry below this fraction of the reference double blur_absolute_floor = 0.0; // 0 disables the catastrophic-blur backstop }; struct Verdict { bool accept = false; std::string reason; // "ok" / "clipped" / "dark" / "bright" / "blur" / "saturated" CaptureSettings next; // settings to use for the next attempt (== current if accepting) }; // Judge one attempt. `reference_sharpness` is this angle's recent sharpness; pass // 0 when unknown, which skips the relative blur check. // // Precedence matters: clipping outranks the mean, because a bright sky can blow // out while the average still looks perfectly reasonable - and blown highlights // are unrecoverable, whereas a slightly dark frame is not. Verdict evaluate(const ImageMetrics& m, const CaptureSettings& current, const PolicyParams& p, double reference_sharpness = 0.0); // Rank attempts so "best so far" is well defined: sharpness discounted by how far // the exposure strayed. Higher is better. double score(const ImageMetrics& m, const PolicyParams& p); } // namespace fgc