blob: cc98154787f5280fac5c3c1b06b63852617411a9 [file] [log] [blame]
/*
* Copyright 2021 The Chromium OS Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "features/gcam_ae/ae_state_machine.h"
#include <algorithm>
#include <cmath>
#include <string>
#include "common/reloadable_config_file.h"
#include "cros-camera/common.h"
namespace cros {
namespace {
constexpr char kSmallStepLog2[] = "small_step_log2";
constexpr char kLargeStepLog2[] = "large_step_log2";
constexpr char kLogSceneBrightnessThreshold[] =
"log_scene_brightness_threshold";
constexpr char kTetConvergeStabilizeDurationMs[] =
"tet_converge_stabilize_duration_ms";
constexpr char kTetConvergeThresholdLog2[] = "tet_converge_threshold_log2";
constexpr char kTetRescanThresholdLog2[] = "tet_rescan_threshold_log2";
constexpr char kTetRetentionDurationMsDefault[] =
"tet_retention_duration_ms_default";
constexpr char kTetRetentionDurationMsWithFace[] =
"tet_retention_duration_ms_with_face";
constexpr char kTetTargetThresholdLog2[] = "tet_target_threshold_log2";
constexpr char kHdrRatioStep[] = "hdr_ratio_step";
// The log2 IIR filter strength for the long/short TET computed by Gcam AE.
constexpr float kFilterStrength = 0.85f;
constexpr float kTetEpsilon = 1.0e-8f;
// IIR filter on log2 space:
// exp2(|strength| * log2(current_value) + (1 - |strength|) * log2(new_value))
float IirFilterLog2(float current_value, float new_value, float strength) {
if (current_value > kTetEpsilon && new_value > kTetEpsilon) {
const float curr_log = std::log2f(current_value);
const float new_log = std::log2f(new_value);
const float next_log = strength * curr_log + (1 - strength) * new_log;
return std::max(std::exp2f(next_log), kTetEpsilon);
}
return current_value;
}
// Gets a smoothed TET value moving from |previous| to |target| with no more
// than |step_log2| difference in the log2 space.
float SmoothTetTransition(const float target,
const float previous,
const float step_log2) {
if (target > kTetEpsilon && previous > kTetEpsilon) {
const float prev_log = std::log2f(previous);
if (target > previous) {
return std::min(target, std::exp2f(prev_log + step_log2));
} else {
return std::max(target, std::exp2f(prev_log - step_log2));
}
}
return target;
}
// Gets a smoothed HDR ratio value moving from |previous| to |target| with no
// more than |step| difference.
float SmoothHdrRatioTransition(const float target,
const float previous,
const float step) {
if (target > previous) {
return std::min(target, previous + step);
} else {
return std::max(target, previous - step);
}
}
int ElapsedTimeMs(base::TimeTicks since) {
return (base::TimeTicks::Now() - since).InMilliseconds();
}
} // namespace
std::string AeStateMachine::ExposureDescriptor::ToString() const {
return base::StringPrintf("{tet=%f, hdr_ratio=%f, log_scene_brightness=%f}",
tet, hdr_ratio, log_scene_brightness);
}
AeStateMachine::AeStateMachine()
: tet_step_log2_(tuning_parameters_.large_step_log2),
camera_metrics_(CameraMetrics::New()) {}
AeStateMachine::~AeStateMachine() {
UploadMetrics();
}
void AeStateMachine::OnNewAeParameters(InputParameters inputs,
MetadataLogger* metadata_logger) {
base::AutoLock lock(lock_);
const AeFrameInfo& frame_info = inputs.ae_frame_info;
const AeParameters& raw_ae_parameters = inputs.ae_parameters;
VLOGFID(1, frame_info.frame_number)
<< "Raw AE parameters:"
<< " short_tet=" << raw_ae_parameters.short_tet
<< " long_tet=" << raw_ae_parameters.long_tet
<< " log_scene_brightness=" << raw_ae_parameters.log_scene_brightness;
// Filter the TET transition to avoid AE fluctuations or hunting.
float prev_short_tet = current_ae_parameters_.short_tet;
float prev_long_tet = current_ae_parameters_.long_tet;
if (!current_ae_parameters_.IsValid()) {
// This is the first set of AE parameters we get.
prev_short_tet = tuning_parameters_.initial_tet;
prev_long_tet =
tuning_parameters_.initial_tet * tuning_parameters_.initial_hdr_ratio;
// Initialize |next_| with the initial TET and HDR ratio to jump start the
// AE loop.
next_.tet = tuning_parameters_.initial_tet;
next_.hdr_ratio = tuning_parameters_.initial_hdr_ratio;
}
current_ae_parameters_.short_tet = IirFilterLog2(
prev_short_tet, raw_ae_parameters.short_tet, kFilterStrength);
current_ae_parameters_.long_tet =
IirFilterLog2(prev_long_tet, raw_ae_parameters.long_tet, kFilterStrength);
current_ae_parameters_.log_scene_brightness =
raw_ae_parameters.log_scene_brightness;
const float hdr_ratio =
current_ae_parameters_.long_tet / current_ae_parameters_.short_tet;
VLOGFID(1, frame_info.frame_number)
<< "Filtered AE parameters:"
<< " short_tet=" << current_ae_parameters_.short_tet
<< " long_tet=" << current_ae_parameters_.long_tet
<< " hdr_ratio=" << hdr_ratio;
gcam_ae_metrics_.accumulated_hdr_ratio += hdr_ratio;
++gcam_ae_metrics_.num_hdr_ratio_samples;
gcam_ae_metrics_.accumulated_tet += current_ae_parameters_.short_tet;
++gcam_ae_metrics_.num_tet_samples;
const float new_tet = current_ae_parameters_.short_tet;
const float new_hdr_ratio =
current_ae_parameters_.long_tet / current_ae_parameters_.short_tet;
const float actual_tet_set = frame_info.exposure_time_ms *
frame_info.analog_gain * frame_info.digital_gain;
auto get_tet_step = [&](float target_brightness,
float previous_brightness) -> float {
float tet_step = tuning_parameters_.large_step_log2;
if (std::fabsf(target_brightness - previous_brightness) <
tuning_parameters_.log_scene_brightness_threshold) {
tet_step = tuning_parameters_.small_step_log2;
}
VLOGFID(1, frame_info.frame_number)
<< "target_brightness=" << target_brightness;
VLOGFID(1, frame_info.frame_number)
<< "previous_brightness=" << previous_brightness;
VLOGFID(1, frame_info.frame_number) << "tet_step=" << tet_step;
return tet_step;
};
// Compute state transition.
MaybeToggleAeLock(frame_info);
State next_state;
switch (current_state_) {
case State::kInactive:
next_state = State::kSearching;
// For camera cold start.
convergence_starting_frame_ = frame_info.frame_number;
break;
case State::kSearching:
SearchTargetTet(frame_info, inputs, new_tet);
if (target_) {
next_state = State::kConverging;
} else {
next_state = State::kSearching;
}
break;
case State::kConverging: {
SearchTargetTet(frame_info, inputs, new_tet);
if (!target_) {
next_state = State::kSearching;
break;
}
ConvergeToTargetTet(frame_info, inputs, actual_tet_set);
if (converged_) {
if (ae_locked_) {
next_state = State::kLocked;
} else {
if (converged_start_time_ &&
ElapsedTimeMs(*converged_start_time_) >
tuning_parameters_.tet_converge_stabilize_duration_ms) {
next_state = State::kConverged;
// Record convergence latency whenever we transition to the
// Converged state. Only count the metrics here so that we exclude
// the AE lock convergence latency.
if (convergence_starting_frame_ != kInvalidFrame) {
gcam_ae_metrics_.accumulated_convergence_latency_frames +=
frame_info.frame_number - convergence_starting_frame_;
++gcam_ae_metrics_.num_convergence_samples;
convergence_starting_frame_ = kInvalidFrame;
}
break;
}
if (!converged_start_time_) {
converged_start_time_ = base::TimeTicks::Now();
}
next_state = State::kConverging;
}
} else {
converged_start_time_.reset();
next_state = State::kConverging;
}
break;
}
case State::kConverged: {
SearchTargetTet(frame_info, inputs, new_tet);
if (ae_locked_) {
next_state = State::kConverging;
break;
}
// Avoid changing TET when the frame brightness change is less than
// |tuning_parameters_.tet_rescan_threshold_log2|.
if (target_ &&
std::fabs(std::log2f(converged_->tet) - std::log2f(target_->tet)) <=
tuning_parameters_.tet_rescan_threshold_log2) {
last_converged_time_ = base::TimeTicks::Now();
next_state = State::kConverged;
break;
}
// Searching for or converging to a new TET target if we observed that the
// frame brightness has chnaged for more than |tet_retention_duration_ms_|
// ms.
if (ElapsedTimeMs(last_converged_time_) > *tet_retention_duration_ms_) {
if (target_) {
next_state = State::kConverging;
tet_step_log2_ = get_tet_step(target_->log_scene_brightness,
converged_->log_scene_brightness);
} else {
next_state = State::kSearching;
tet_step_log2_ =
get_tet_step(current_ae_parameters_.log_scene_brightness,
converged_->log_scene_brightness);
}
// Start convergence timer whenever we transition out of the Converged
// state.
convergence_starting_frame_ = frame_info.frame_number;
break;
} else {
next_state = State::kConverged;
}
break;
}
case State::kLocked:
SearchTargetTet(frame_info, inputs, new_tet);
if (ae_locked_) {
DCHECK(target_);
if (std::fabs(std::log2f(actual_tet_set) - std::log2f(target_->tet)) <=
tuning_parameters_.tet_rescan_threshold_log2) {
next_state = State::kLocked;
} else {
next_state = State::kConverging;
}
} else {
if (!target_) {
next_state = State::kSearching;
tet_step_log2_ =
get_tet_step(current_ae_parameters_.log_scene_brightness,
converged_->log_scene_brightness);
} else {
next_state = State::kConverging;
// Determine the TET transition step bound on the brightness
// difference.
tet_step_log2_ = get_tet_step(target_->log_scene_brightness,
converged_->log_scene_brightness);
}
}
break;
}
VLOGFID(1, frame_info.frame_number)
<< "state=" << current_state_ << " next_state=" << next_state
<< " actual_tet_set=" << actual_tet_set;
// Execute state entry actions.
switch (next_state) {
case State::kInactive:
break;
case State::kSearching: {
next_.tet = SmoothTetTransition(new_tet, next_.tet, tet_step_log2_);
next_.hdr_ratio = SmoothHdrRatioTransition(
new_hdr_ratio, next_.hdr_ratio, tuning_parameters_.hdr_ratio_step);
next_.log_scene_brightness = current_ae_parameters_.log_scene_brightness;
break;
}
case State::kConverging: {
next_.tet = SmoothTetTransition(target_->tet, next_.tet, tet_step_log2_);
next_.hdr_ratio =
SmoothHdrRatioTransition(target_->hdr_ratio, next_.hdr_ratio,
tuning_parameters_.hdr_ratio_step);
next_.log_scene_brightness = current_ae_parameters_.log_scene_brightness;
break;
}
case State::kConverged:
next_.tet = converged_->tet;
next_.hdr_ratio =
SmoothHdrRatioTransition(converged_->hdr_ratio, next_.hdr_ratio,
tuning_parameters_.hdr_ratio_step);
next_.log_scene_brightness = converged_->log_scene_brightness;
break;
case State::kLocked:
DCHECK(converged_);
// AE compensation is still effective when AE is locked. |converged_| here
// has the TET value with AE compensation applied on top of the TET stored
// in |locked_|.
next_.tet = converged_->tet;
next_.hdr_ratio = locked_->hdr_ratio;
next_.log_scene_brightness = locked_->log_scene_brightness;
break;
}
constexpr int kInvalidDuration = -1;
VLOGFID(1, frame_info.frame_number)
<< "target=" << (target_ ? target_->ToString() : "n/a");
VLOGFID(1, frame_info.frame_number)
<< "converged=" << (converged_ ? converged_->ToString() : "n/a");
VLOGFID(1, frame_info.frame_number)
<< "tet_retention_duration_ms="
<< (tet_retention_duration_ms_ ? *tet_retention_duration_ms_
: kInvalidDuration);
VLOGFID(1, frame_info.frame_number) << "ae_locked=" << ae_locked_;
VLOGFID(1, frame_info.frame_number)
<< "locked=" << (locked_ ? locked_->ToString() : "n/a");
VLOGFID(1, frame_info.frame_number) << "next=" << next_.ToString();
if (metadata_logger) {
metadata_logger->Log(frame_info.frame_number, kTagShortTet,
raw_ae_parameters.short_tet);
metadata_logger->Log(frame_info.frame_number, kTagLongTet,
raw_ae_parameters.long_tet);
metadata_logger->Log(frame_info.frame_number, kTagLogSceneBrightness,
raw_ae_parameters.log_scene_brightness);
metadata_logger->Log(frame_info.frame_number, kTagFilteredShortTet,
current_ae_parameters_.short_tet);
metadata_logger->Log(frame_info.frame_number, kTagFilteredLongTet,
current_ae_parameters_.long_tet);
metadata_logger->Log(frame_info.frame_number, kTagAeState,
static_cast<int32_t>(current_state_));
metadata_logger->Log(frame_info.frame_number, kTagActualTet,
actual_tet_set);
}
previous_tet_ = new_tet;
current_state_ = next_state;
}
void AeStateMachine::OnReset() {
base::AutoLock lock(lock_);
current_state_ = State::kInactive;
previous_tet_ = 0;
next_ = ExposureDescriptor();
target_.reset();
converged_.reset();
converged_start_time_.reset();
tet_retention_duration_ms_.reset();
locked_.reset();
ae_locked_ = false;
}
void AeStateMachine::OnOptionsUpdated(const base::Value& json_values) {
base::AutoLock lock(lock_);
LoadIfExist(json_values, kTetTargetThresholdLog2,
&tuning_parameters_.tet_target_threshold_log2);
LoadIfExist(json_values, kSmallStepLog2, &tuning_parameters_.small_step_log2);
LoadIfExist(json_values, kLargeStepLog2, &tuning_parameters_.large_step_log2);
LoadIfExist(json_values, kLogSceneBrightnessThreshold,
&tuning_parameters_.log_scene_brightness_threshold);
LoadIfExist(json_values, kTetConvergeStabilizeDurationMs,
&tuning_parameters_.tet_converge_stabilize_duration_ms);
LoadIfExist(json_values, kTetConvergeThresholdLog2,
&tuning_parameters_.tet_converge_threshold_log2);
LoadIfExist(json_values, kTetRescanThresholdLog2,
&tuning_parameters_.tet_rescan_threshold_log2);
LoadIfExist(json_values, kTetRetentionDurationMsDefault,
&tuning_parameters_.tet_retention_duration_ms_default);
LoadIfExist(json_values, kTetRetentionDurationMsWithFace,
&tuning_parameters_.tet_retention_duration_ms_with_face);
LoadIfExist(json_values, kInitialTet, &tuning_parameters_.initial_tet);
LoadIfExist(json_values, kInitialHdrRatio,
&tuning_parameters_.initial_hdr_ratio);
LoadIfExist(json_values, kHdrRatioStep, &tuning_parameters_.hdr_ratio_step);
if (VLOG_IS_ON(1)) {
VLOGF(1) << "AeStateMachine tuning parameters:"
<< " tet_target_threshold_log2="
<< tuning_parameters_.tet_target_threshold_log2
<< " small_step_log2=" << tuning_parameters_.small_step_log2
<< " large_step_log2=" << tuning_parameters_.large_step_log2
<< " log_scene_brightness_threshold="
<< tuning_parameters_.log_scene_brightness_threshold
<< " tet_converge_stabilize_duration_ms="
<< tuning_parameters_.tet_converge_stabilize_duration_ms
<< " tet_converge_threshold_log2="
<< tuning_parameters_.tet_converge_threshold_log2
<< " tet_rescan_threshold_log2="
<< tuning_parameters_.tet_rescan_threshold_log2
<< " tet_retention_duration_ms_default="
<< tuning_parameters_.tet_retention_duration_ms_default
<< " tet_retention_duration_ms_with_face="
<< tuning_parameters_.tet_retention_duration_ms_with_face
<< " initial_tet=" << tuning_parameters_.initial_tet
<< " hdr_ratio_step=" << tuning_parameters_.hdr_ratio_step;
}
}
float AeStateMachine::GetCaptureTet() {
base::AutoLock lock(lock_);
return next_.tet;
}
float AeStateMachine::GetFilteredHdrRatio() {
base::AutoLock lock(lock_);
return next_.hdr_ratio;
}
uint8_t AeStateMachine::GetAndroidAeState() {
// We don't support flash, so there's no FLASH_REQUIRED state.
switch (current_state_) {
case AeStateMachine::State::kInactive:
return ANDROID_CONTROL_AE_STATE_INACTIVE;
case AeStateMachine::State::kSearching:
case AeStateMachine::State::kConverging:
return ANDROID_CONTROL_AE_STATE_SEARCHING;
case AeStateMachine::State::kConverged:
return ANDROID_CONTROL_AE_STATE_CONVERGED;
case AeStateMachine::State::kLocked:
return ANDROID_CONTROL_AE_STATE_LOCKED;
}
}
std::ostream& operator<<(std::ostream& os, AeStateMachine::State state) {
std::string state_str;
switch (state) {
case AeStateMachine::State::kInactive:
state_str = "Inactive";
break;
case AeStateMachine::State::kSearching:
state_str = "Searching";
break;
case AeStateMachine::State::kConverging:
state_str = "Converging";
break;
case AeStateMachine::State::kConverged:
state_str = "Converged";
break;
case AeStateMachine::State::kLocked:
state_str = "Locked";
break;
}
return os << state_str;
}
void AeStateMachine::SearchTargetTet(const AeFrameInfo& frame_info,
const InputParameters& inputs,
const float new_tet) {
if (ae_locked_) {
// AE compensation is still effective when AE is locked.
target_ = {
.tet =
locked_->tet * std::exp2f(frame_info.client_ae_compensation_log2),
.hdr_ratio = locked_->hdr_ratio,
.log_scene_brightness = locked_->log_scene_brightness,
};
return;
}
const float previous_log = std::log2f(previous_tet_);
const float new_log = std::log2f(new_tet);
const float search_tet_delta_log = std::fabs(previous_log - new_log);
VLOGFID(1, frame_info.frame_number)
<< "search_tet_delta_log=" << search_tet_delta_log;
if (search_tet_delta_log <= tuning_parameters_.tet_target_threshold_log2) {
target_ = {
// Make sure we set a target TET that's achievable by the camera.
.tet = inputs.tet_range.Clamp(new_tet),
.hdr_ratio =
current_ae_parameters_.long_tet / current_ae_parameters_.short_tet,
.log_scene_brightness = current_ae_parameters_.log_scene_brightness,
};
} else {
target_.reset();
}
}
void AeStateMachine::ConvergeToTargetTet(const AeFrameInfo& frame_info,
const InputParameters& inputs,
const float actual_tet_set) {
const float actual_tet_set_log = std::log2f(actual_tet_set);
const float converge_tet_delta_log =
std::fabs(actual_tet_set_log - std::log2f(target_->tet));
VLOGFID(1, frame_info.frame_number)
<< "converge_tet_delta_log=" << converge_tet_delta_log;
if (converge_tet_delta_log < tuning_parameters_.tet_converge_threshold_log2) {
converged_ = *target_;
tet_retention_duration_ms_ =
frame_info.faces->empty()
? tuning_parameters_.tet_retention_duration_ms_default
: tuning_parameters_.tet_retention_duration_ms_with_face;
} else {
converged_.reset();
tet_retention_duration_ms_.reset();
}
}
void AeStateMachine::MaybeToggleAeLock(const AeFrameInfo& frame_info) {
if (frame_info.client_request_settings.ae_lock) {
if (*frame_info.client_request_settings.ae_lock ==
ANDROID_CONTROL_AE_LOCK_ON &&
!ae_locked_) {
ae_locked_ = true;
locked_ = next_;
} else if (*frame_info.client_request_settings.ae_lock ==
ANDROID_CONTROL_AE_LOCK_OFF &&
ae_locked_) {
ae_locked_ = false;
locked_.reset();
}
}
}
void AeStateMachine::UploadMetrics() {
base::AutoLock lock(lock_);
if (gcam_ae_metrics_.num_convergence_samples > 0) {
camera_metrics_->SendGcamAeAvgConvergenceLatency(
gcam_ae_metrics_.accumulated_convergence_latency_frames /
gcam_ae_metrics_.num_convergence_samples);
}
if (gcam_ae_metrics_.num_hdr_ratio_samples > 0) {
camera_metrics_->SendGcamAeAvgHdrRatio(
gcam_ae_metrics_.accumulated_hdr_ratio /
gcam_ae_metrics_.num_hdr_ratio_samples);
}
if (gcam_ae_metrics_.num_tet_samples > 0) {
camera_metrics_->SendGcamAeAvgTet(gcam_ae_metrics_.accumulated_tet /
gcam_ae_metrics_.num_tet_samples);
}
}
} // namespace cros