139 lines
4.6 KiB
C++
139 lines
4.6 KiB
C++
#include "fgc/SettleRule.h"
|
|
|
|
#include <doctest/doctest.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <fstream>
|
|
#include <map>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using namespace fgc;
|
|
|
|
namespace {
|
|
|
|
FrameStats frame(double b, double e = 20000, double g = 15) {
|
|
static uint64_t seq = 0;
|
|
return FrameStats{++seq, b, e, g};
|
|
}
|
|
|
|
struct Row {
|
|
double t, exposure, gain, brightness;
|
|
};
|
|
|
|
// The 24 auto-exposure steps recorded on towers 121 and 111 (tests/data/ae_steps.csv).
|
|
std::map<int, std::vector<Row>> recordedSteps() {
|
|
std::map<int, std::vector<Row>> runs;
|
|
std::ifstream f(std::string(FGC_SOURCE_DIR) + "/tests/data/ae_steps.csv");
|
|
std::string line;
|
|
while (std::getline(f, line)) {
|
|
if (line.empty() || line[0] == '#') continue;
|
|
std::stringstream ss(line);
|
|
std::string field;
|
|
std::vector<std::string> v;
|
|
while (std::getline(ss, field, ',')) v.push_back(field);
|
|
if (v.size() != 8) continue;
|
|
runs[std::stoi(v[0])].push_back({std::stod(v[4]), std::stod(v[5]), std::stod(v[6]),
|
|
std::stod(v[7])});
|
|
}
|
|
return runs;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("settled: in the camera's band and steady for 3 frames") {
|
|
SettleRule r; // band 50 +- 5 % of 255 = 114.75 .. 140.25
|
|
CHECK(r.add(frame(120)) == SettleRule::Verdict::Waiting);
|
|
CHECK(r.add(frame(121)) == SettleRule::Verdict::Waiting);
|
|
CHECK(r.add(frame(121.5)) == SettleRule::Verdict::Settled);
|
|
}
|
|
|
|
TEST_CASE("not settled while brightness is still moving through the band") {
|
|
SettleRule r;
|
|
r.add(frame(139));
|
|
r.add(frame(131)); // -6%: still coming down
|
|
CHECK(r.add(frame(124)) == SettleRule::Verdict::Waiting);
|
|
CHECK(r.add(frame(122)) == SettleRule::Verdict::Waiting); // 124 -> 122 steady, 131 -> 124 not
|
|
CHECK(r.add(frame(122)) == SettleRule::Verdict::Settled);
|
|
}
|
|
|
|
TEST_CASE("a pause outside the band is not settled (auto-exposure stalls mid-way)") {
|
|
SettleRule r;
|
|
for (int i = 0; i < 10; ++i) CHECK(r.add(frame(157, 20000, 19.6)) == SettleRule::Verdict::Waiting);
|
|
}
|
|
|
|
TEST_CASE("steady but too dark at full exposure and gain: waiting cannot help") {
|
|
SettleRule::Params p;
|
|
p.exposure_max_us = 20000;
|
|
p.gain_max_db = 24;
|
|
SettleRule r(p);
|
|
r.add(frame(40, 20000, 24));
|
|
r.add(frame(40.2, 20000, 24));
|
|
CHECK(r.add(frame(40.1, 20000, 24)) == SettleRule::Verdict::AtLimit);
|
|
// Below the limits it is just slow, not stuck.
|
|
SettleRule s(p);
|
|
s.add(frame(40, 20000, 12));
|
|
s.add(frame(40, 20000, 12));
|
|
CHECK(s.add(frame(40, 20000, 12)) == SettleRule::Verdict::Waiting);
|
|
}
|
|
|
|
TEST_CASE("steady but too bright at minimum exposure and gain") {
|
|
SettleRule::Params p;
|
|
p.exposure_min_us = 82;
|
|
p.gain_min_db = 0;
|
|
SettleRule r(p);
|
|
r.add(frame(200, 82, 0));
|
|
r.add(frame(200, 82, 0));
|
|
CHECK(r.add(frame(200, 82, 0)) == SettleRule::Verdict::AtLimit);
|
|
}
|
|
|
|
TEST_CASE("a frame without brightness starts the count again") {
|
|
SettleRule r;
|
|
r.add(frame(120));
|
|
r.add(frame(120));
|
|
CHECK(r.add(frame(-1)) == SettleRule::Verdict::Waiting);
|
|
CHECK(r.add(frame(120)) == SettleRule::Verdict::Waiting);
|
|
CHECK(r.add(frame(120)) == SettleRule::Verdict::Waiting);
|
|
CHECK(r.add(frame(120)) == SettleRule::Verdict::Settled);
|
|
}
|
|
|
|
TEST_CASE("the band follows the camera's target and tolerance") {
|
|
SettleRule::Params p;
|
|
p.target_pct = 40;
|
|
p.tolerance_pct = 10;
|
|
SettleRule r(p);
|
|
CHECK(r.bandLow() == doctest::Approx(76.5));
|
|
CHECK(r.bandHigh() == doctest::Approx(127.5));
|
|
}
|
|
|
|
TEST_CASE("replayed on the 24 recorded exposure steps, it keeps a good frame at least 21 times") {
|
|
const auto runs = recordedSteps();
|
|
REQUIRE(runs.size() == 24);
|
|
int correct = 0, early = 0, never = 0;
|
|
for (const auto& [id, rows] : runs) {
|
|
// Where the run finally settles: median brightness over its last 2 s.
|
|
std::vector<double> tail;
|
|
for (const auto& r : rows)
|
|
if (r.t >= rows.back().t - 2.0) tail.push_back(r.brightness);
|
|
std::sort(tail.begin(), tail.end());
|
|
const double final_level = tail[tail.size() / 2];
|
|
|
|
SettleRule rule;
|
|
bool fired = false;
|
|
for (const auto& r : rows) {
|
|
if (rule.add(FrameStats{0, r.brightness, r.exposure, r.gain}) !=
|
|
SettleRule::Verdict::Waiting) {
|
|
fired = true;
|
|
(std::fabs(r.brightness / final_level - 1.0) <= 0.10 ? correct : early)++;
|
|
break;
|
|
}
|
|
}
|
|
never += !fired;
|
|
}
|
|
INFO("correct " << correct << ", early " << early << ", never " << never);
|
|
CHECK(correct >= 21);
|
|
CHECK(never == 0);
|
|
}
|