216 lines
8.4 KiB
C++
216 lines
8.4 KiB
C++
#pragma once
|
|
|
|
#include <opencv2/core.hpp>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace target_exposure {
|
|
|
|
// All timestamps use caller-supplied, nonnegative microseconds in one clock domain.
|
|
using TimestampUs = std::int64_t;
|
|
enum class State { CAPTURE, TRACK, LOST };
|
|
enum class ResetReason { Startup, Manual, NewBusinessCycle, ClockChanged,
|
|
ConfigurationChanged, IdleTimeout, HistoryExpired };
|
|
enum class MeterStatus { Valid, NoTarget, EmptyImage, UnsupportedImage,
|
|
InvalidRoi, EmptyRoi, InsufficientPixels, InvalidConfiguration };
|
|
enum class PredictionReason { Available, Disabled, ModelNotConfirmed,
|
|
CurrentMeasurementInvalid, InsufficientHistory, MissingHorizon,
|
|
InvalidHorizon, DarkOrSaturated, ResidualTooLarge, OutOfRange, InvalidNumbers };
|
|
enum class Reason { Initialized, Ready, InvalidConfiguration, InvalidTimestamp,
|
|
DuplicateFrame, OutOfOrderFrame, PreCaptureRequired, InvalidActualExposure,
|
|
ConfirmingTarget, TrackingActivated, InvalidMetering, Deadband, Hysteresis,
|
|
ConfirmingError, ProportionalAdjustment, PiAdjustment, ExposureLimit,
|
|
TargetMissing, EnteredLost, Frozen, CaptureAfterFreeze, RecoveryNotAllowed,
|
|
RecoveryReferenceMissing, RecoveryNotNeeded, RecoverySuggested, NotRecovered };
|
|
|
|
const char* toString(State value) noexcept;
|
|
const char* toString(ResetReason value) noexcept;
|
|
const char* toString(MeterStatus value) noexcept;
|
|
const char* toString(PredictionReason value) noexcept;
|
|
const char* toString(Reason value) noexcept;
|
|
|
|
struct MeteringConfig {
|
|
double trim_fraction_each_side = 0.05;
|
|
std::size_t min_target_pixels = 16;
|
|
std::size_t min_retained_pixels = 8;
|
|
// 1 = whole image; otherwise a fixed centered rectangle of this width/height fraction.
|
|
double global_center_fraction = 1.0;
|
|
int near_black_level = 2;
|
|
int saturation_level = 253;
|
|
};
|
|
struct PredictionConfig {
|
|
bool enabled = true;
|
|
// Set only when gain/response and known black offset support the linear model.
|
|
bool photometric_model_confirmed = false;
|
|
std::size_t points = 3; // exactly 3 or 5, no implicit 5-to-3 fallback
|
|
double black_level = 0.0;
|
|
TimestampUs nominal_interval_us = 0; // 0 requires next_capture_timestamp_us
|
|
TimestampUs max_horizon_us = 60'000'000;
|
|
double max_relative_fit_rmse = 0.10;
|
|
double max_near_black_fraction = 0.10;
|
|
double max_saturated_fraction = 0.05;
|
|
double min_predicted_gray = 1.0;
|
|
double max_predicted_gray = 254.0;
|
|
};
|
|
struct ControllerConfig {
|
|
double target_gray = 118.0;
|
|
double deadband_ratio = 0.10;
|
|
double hysteresis_ratio = 0.02;
|
|
double large_error_ratio = 0.40;
|
|
double proportional_gain = 0.70;
|
|
double kp = 0.40;
|
|
double ki = 0.10;
|
|
double integral_abs_limit = 1.0;
|
|
double max_step_ratio = 0.25; // applies to both proportional and PI branches
|
|
int same_direction_frames = 2; // small error only, matching the approved pseudocode
|
|
};
|
|
struct RecoveryConfig {
|
|
std::optional<double> global_reference_gray;
|
|
double deadband_ratio = 0.30;
|
|
double gain = 0.30;
|
|
double max_step_ratio = 0.10;
|
|
};
|
|
struct Config {
|
|
std::string profile_id = "default";
|
|
std::uint64_t revision = 0;
|
|
// Required from the caller. No motion-scene exposure maximum is hardcoded.
|
|
double min_exposure_us = 0.0;
|
|
double max_exposure_us = 0.0;
|
|
double empirical_exposure_us = 0.0;
|
|
TimestampUs history_window_us = 300'000'000;
|
|
TimestampUs idle_timeout_us = 300'000'000;
|
|
std::size_t history_capacity = 512;
|
|
int detect_confirm_frames = 3;
|
|
int lost_confirm_frames = 3;
|
|
int freeze_frames = 2;
|
|
MeteringConfig metering;
|
|
PredictionConfig prediction;
|
|
ControllerConfig controller;
|
|
RecoveryConfig recovery;
|
|
};
|
|
|
|
std::vector<std::string> validate(const Config& config);
|
|
bool operator==(const Config& a, const Config& b);
|
|
inline bool operator!=(const Config& a, const Config& b) { return !(a == b); }
|
|
|
|
struct Roi {
|
|
std::vector<cv::Rect> rectangles;
|
|
std::vector<cv::RotatedRect> rotated_rectangles;
|
|
// Each nonzero CV_8UC1 pixel is included. Full image dimensions are required.
|
|
std::vector<cv::Mat> masks;
|
|
};
|
|
// Explicit helpers only; processFrame never calls these on the supplied final ROI.
|
|
cv::Rect inset(const cv::Rect& rectangle, double fraction_per_side);
|
|
cv::RotatedRect inset(const cv::RotatedRect& rectangle, double fraction_per_side);
|
|
|
|
struct Brightness {
|
|
// Existing statistic: target trimmed mean; global untrimmed mean.
|
|
std::optional<double> gray;
|
|
MeterStatus status = MeterStatus::EmptyImage;
|
|
std::size_t pixels = 0;
|
|
std::size_t retained_pixels = 0;
|
|
double near_black_fraction = 0.0;
|
|
double saturated_fraction = 0.0;
|
|
// Ordinary mean before trimming; absent whenever the measurement is invalid.
|
|
std::optional<double> arithmetic_gray;
|
|
bool valid() const noexcept { return status == MeterStatus::Valid && gray.has_value(); }
|
|
};
|
|
struct MeteringResult { Brightness target; Brightness global; };
|
|
|
|
// Stateless Gray8/BGR8 measurement. No exposure, timestamp or control instance needed.
|
|
// hasUsableRegion describes the supplied ROI, not confirmed physical target presence.
|
|
// Reads image/masks synchronously without retaining or changing them. Calls may run
|
|
// concurrently when the caller keeps the input image/mask data unchanged.
|
|
MeteringResult measureImage(const cv::Mat& image, const Roi& roi, bool hasUsableRegion,
|
|
const MeteringConfig& config = {});
|
|
|
|
struct HistoryRecord {
|
|
TimestampUs timestamp_us = 0;
|
|
double actual_exposure_us = 0.0;
|
|
bool has_usable_target = false;
|
|
MeteringResult metering;
|
|
std::string profile_id;
|
|
std::uint64_t config_revision = 0;
|
|
};
|
|
struct PredictionResult {
|
|
PredictionReason reason = PredictionReason::InsufficientHistory;
|
|
bool available = false;
|
|
bool adopted = false;
|
|
std::size_t points_used = 0;
|
|
std::optional<TimestampUs> timestamp_us;
|
|
std::optional<double> predicted_gray;
|
|
std::optional<double> normalized_slope_per_second;
|
|
std::optional<double> relative_fit_rmse;
|
|
};
|
|
struct ControlTrace {
|
|
std::optional<double> current_error_ratio;
|
|
std::optional<double> control_error_ratio;
|
|
double integral = 0.0;
|
|
int same_direction_count = 0;
|
|
double raw_delta_exposure_us = 0.0;
|
|
double limited_delta_exposure_us = 0.0;
|
|
bool integral_blocked_by_limit = false;
|
|
bool step_limited = false;
|
|
bool exposure_limited = false;
|
|
};
|
|
struct FrameInput {
|
|
cv::Mat image; // Gray8 or BGR8, read synchronously; no image retained after the call
|
|
Roi roi;
|
|
bool has_usable_target = false;
|
|
TimestampUs timestamp_us = 0;
|
|
double actual_exposure_us = 0.0; // finite and inside the configured limits
|
|
std::optional<TimestampUs> next_capture_timestamp_us;
|
|
bool allow_no_target_recovery = false;
|
|
};
|
|
struct PreCaptureResult {
|
|
bool accepted = false;
|
|
State state = State::CAPTURE;
|
|
bool initialization_required = false;
|
|
std::optional<double> suggested_exposure_us;
|
|
Reason reason = Reason::PreCaptureRequired;
|
|
std::optional<ResetReason> initialization_reason;
|
|
std::vector<std::string> errors;
|
|
};
|
|
struct FrameResult {
|
|
bool accepted = false;
|
|
State state = State::CAPTURE;
|
|
Reason reason = Reason::PreCaptureRequired;
|
|
double actual_exposure_us = 0.0;
|
|
bool adjustment_suggested = false;
|
|
std::optional<double> suggested_exposure_us;
|
|
MeteringResult metering;
|
|
std::size_t valid_history_count = 0;
|
|
PredictionResult prediction;
|
|
ControlTrace control;
|
|
int detection_count = 0;
|
|
int loss_count = 0;
|
|
int freeze_remaining = 0;
|
|
bool loss_episode_active = false;
|
|
bool recovery_used = false;
|
|
};
|
|
|
|
// One instance per independent camera/stream. Serialize calls to the same instance.
|
|
class ExposureControl {
|
|
public:
|
|
ExposureControl();
|
|
~ExposureControl();
|
|
ExposureControl(const ExposureControl&) = delete;
|
|
ExposureControl& operator=(const ExposureControl&) = delete;
|
|
PreCaptureResult preCapture(TimestampUs now_us, const Config& config);
|
|
FrameResult processFrame(const FrameInput& input);
|
|
// Only NewBusinessCycle opens a new recovery budget without stable detections.
|
|
// ClockChanged also clears the timestamp watermark; it preserves the loss budget.
|
|
void reset(ResetReason reason);
|
|
State state() const noexcept;
|
|
std::vector<HistoryRecord> history() const;
|
|
private:
|
|
struct Impl;
|
|
std::unique_ptr<Impl> impl_;
|
|
};
|
|
|
|
} // namespace target_exposure
|