CommonLibraries/src/continuous_control.cpp
Ben 51e364997a feat: add continuous exposure control and native-depth metering
Add continuous global/ROI feedback with decision tracing, valid-image masks, and exact Gray16 source means alongside normalized metrics. Preserve legacy control behavior and expose the 1.3.0 API.

Validation: Release CTest 12/12 (70 cases), installed consumer 1/1, and diff checks passed. Software-only verification; no real device operation.
2026-09-22 20:45:56 +08:00

111 lines
5.3 KiB
C++

#include "internal.hpp"
#include <cmath>
namespace target_exposure {
namespace {
bool usable(const Brightness& value) {
const auto gray = [](double v) { return std::isfinite(v) && v >= 0 && v <= 255; };
const auto fraction = [](double v) { return std::isfinite(v) && v >= 0 && v <= 1; };
return value.valid() && gray(*value.gray) &&
(!value.arithmetic_gray || gray(*value.arithmetic_gray)) && value.pixels > 0 &&
value.retained_pixels > 0 && value.retained_pixels <= value.pixels &&
fraction(value.near_black_fraction) && fraction(value.saturated_fraction) &&
value.near_black_fraction + value.saturated_fraction <= 1.0 + 1e-12;
}
bool validSource(FeedbackSource source) {
return source == FeedbackSource::Global || source == FeedbackSource::ManualRoi ||
source == FeedbackSource::YoloRegions;
}
}
struct ContinuousExposureControl::Impl {
detail::Controller controller;
std::optional<ContinuousConfig> config;
std::optional<FeedbackSource> source;
std::optional<TimestampUs> watermark;
std::optional<FeedbackResetReason> pending_reset = FeedbackResetReason::Startup;
};
ContinuousExposureControl::ContinuousExposureControl() : impl_(std::make_unique<Impl>()) {}
ContinuousExposureControl::~ContinuousExposureControl() = default;
void ContinuousExposureControl::reset(FeedbackResetReason reason) {
impl_->controller.reset();
impl_->source.reset();
impl_->pending_reset = reason;
if (reason == FeedbackResetReason::ClockChanged) impl_->watermark.reset();
}
ContinuousFrameResult ContinuousExposureControl::update(const ContinuousFrameInput& input,
const ContinuousConfig& config) {
auto& s = *impl_;
ContinuousFrameResult out;
out.requested_source = input.source;
out.actual_exposure_us = input.actual_exposure_us;
out.metering = input.metering;
out.control.integral = s.controller.integral;
out.control.same_direction_count = s.controller.count;
out.errors = validate(config);
if (!validSource(input.source)) out.errors.emplace_back("invalid measurement source");
if (!out.errors.empty()) { out.reason = Reason::InvalidConfiguration; return out; }
if (input.timestamp_us < 0) { out.reason = Reason::InvalidTimestamp; return out; }
if (s.watermark && input.timestamp_us <= *s.watermark) {
out.reason = input.timestamp_us == *s.watermark ? Reason::DuplicateFrame : Reason::OutOfOrderFrame;
return out;
}
if (!std::isfinite(input.actual_exposure_us) || input.actual_exposure_us < config.min_exposure_us ||
input.actual_exposure_us > config.max_exposure_us) {
out.reason = Reason::InvalidActualExposure; return out;
}
FeedbackSource selected = input.source;
const Brightness* measurement = &input.metering.global;
if (input.source != FeedbackSource::Global) {
if (usable(input.metering.target)) measurement = &input.metering.target;
else {
selected = FeedbackSource::Global;
const auto status = input.metering.target.status;
out.fallback_reason = status == MeterStatus::NoTarget || status == MeterStatus::EmptyRoi ||
status == MeterStatus::InsufficientPixels ? FeedbackFallbackReason::RegionUnavailable :
FeedbackFallbackReason::InvalidRegionMeasurement;
}
}
if (!usable(*measurement)) {
out.reason = Reason::InvalidMetering;
out.errors.emplace_back("selected measurement has invalid status, gray, pixel counts or fractions");
return out;
}
out.effective_source = selected;
const auto& controller = selected == FeedbackSource::Global ? config.global_controller : config.region_controller;
out.target_gray = controller.target_gray;
out.control_gray = measurement->gray;
if (s.pending_reset) out.reset_reason = s.pending_reset;
else if (!s.config || config != *s.config) out.reset_reason = FeedbackResetReason::ConfigurationChanged;
else if (!s.source || selected != *s.source) out.reset_reason = FeedbackResetReason::SourceChanged;
else if (s.watermark && input.timestamp_us - *s.watermark > config.idle_timeout_us)
out.reset_reason = FeedbackResetReason::IdleTimeout;
if (out.reset_reason) s.controller.reset();
Config controlConfig;
controlConfig.min_exposure_us = config.min_exposure_us;
controlConfig.max_exposure_us = config.max_exposure_us;
controlConfig.controller = controller;
FrameResult feedback;
feedback.suggested_exposure_us = input.actual_exposure_us;
feedback.control.current_error_ratio = (controller.target_gray - *measurement->gray) / controller.target_gray;
s.controller.update(*measurement->gray, input.actual_exposure_us, controlConfig, feedback);
out.accepted = true;
out.reason = feedback.reason;
out.suggested_exposure_us = feedback.suggested_exposure_us;
out.adjustment_suggested = feedback.adjustment_suggested;
out.control = feedback.control;
out.state = feedback.reason == Reason::ExposureLimit ? FeedbackState::Limited :
feedback.adjustment_suggested ? FeedbackState::Adjusting : FeedbackState::Holding;
s.config = config;
s.source = selected;
s.watermark = input.timestamp_us;
s.pending_reset.reset();
return out;
}
} // namespace target_exposure