Wait for a slow camera's next frame instead of restarting the stream, and read integer camera features for the exposure check
This commit is contained in:
parent
fb0f5117df
commit
62131d8083
|
|
@ -28,6 +28,18 @@ long long firstFrameLimitMs(double exposure_us);
|
||||||
// `failed_restarts` counts restarts since the last frame that arrived.
|
// `failed_restarts` counts restarts since the last frame that arrived.
|
||||||
StallAction onNoFirstFrame(long long waited_ms, double exposure_us, int failed_restarts);
|
StallAction onNoFirstFrame(long long waited_ms, double exposure_us, int failed_restarts);
|
||||||
|
|
||||||
|
// How long a stream that has delivered may go without a newer frame before it
|
||||||
|
// counts as stalled: twice the longer of the exposure and the frame period, plus
|
||||||
|
// a second, and never under 2 s. A slow frame is not a stall - tower 111 runs
|
||||||
|
// multi-second exposures, one frame every 7-9 s, and restarting the stream the
|
||||||
|
// moment a capture fell due before the next frame threw that exposure away, so
|
||||||
|
// the next frame came later still (2026-10-04).
|
||||||
|
long long staleFrameLimitMs(double exposure_us, double frame_period_ms);
|
||||||
|
|
||||||
|
// No frame newer than the last one kept, the newest having arrived
|
||||||
|
// `since_newest_ms` ago: wait for the next one, or restart the stream.
|
||||||
|
StallAction onStaleFrame(long long since_newest_ms, double exposure_us, double frame_period_ms);
|
||||||
|
|
||||||
// A stream that delivered before but has gone stale, and a restart has just
|
// A stream that delivered before but has gone stale, and a restart has just
|
||||||
// failed to bring a fresh frame. Restart again, or reset once restarts have
|
// failed to bring a fresh frame. Restart again, or reset once restarts have
|
||||||
// stopped working.
|
// stopped working.
|
||||||
|
|
|
||||||
|
|
@ -164,12 +164,16 @@ private:
|
||||||
bool has_ = false;
|
bool has_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Read a float/string feature, returning `fallback`/"" if absent (tolerant, for telemetry).
|
// Read a numeric/string feature, returning `fallback`/"" if absent (tolerant, for
|
||||||
|
// telemetry). Numeric takes an integer feature too: some are Int on one model and
|
||||||
|
// Float on another (IntensityControllerTolerance is Int on the Alvium 1800 U-1240c).
|
||||||
double getFloat(const CameraPtr& cam, const char* name, double fallback = 0.0) {
|
double getFloat(const CameraPtr& cam, const char* name, double fallback = 0.0) {
|
||||||
FeaturePtr f;
|
FeaturePtr f;
|
||||||
|
if (SP_ACCESS(cam)->GetFeatureByName(name, f) != VmbErrorSuccess) return fallback;
|
||||||
double v = fallback;
|
double v = fallback;
|
||||||
if (SP_ACCESS(cam)->GetFeatureByName(name, f) == VmbErrorSuccess && f->GetValue(v) == VmbErrorSuccess)
|
if (f->GetValue(v) == VmbErrorSuccess) return v;
|
||||||
return v;
|
VmbInt64_t i = 0;
|
||||||
|
if (f->GetValue(i) == VmbErrorSuccess) return static_cast<double>(i);
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
std::string getString(const CameraPtr& cam, const char* name) {
|
std::string getString(const CameraPtr& cam, const char* name) {
|
||||||
|
|
@ -278,6 +282,7 @@ struct VimbaCameraSource::Impl {
|
||||||
// when the current watch began.
|
// when the current watch began.
|
||||||
std::vector<AutoExposure> watch_ae;
|
std::vector<AutoExposure> watch_ae;
|
||||||
std::vector<long long> watch_from;
|
std::vector<long long> watch_from;
|
||||||
|
bool watch_unknown_logged = false; // once per watch
|
||||||
|
|
||||||
// Begin/end a free-run acquisition session (fresh observers each time). Split out so
|
// Begin/end a free-run acquisition session (fresh observers each time). Split out so
|
||||||
// trigger() can restart the stream when it stalls. Stats persist across restarts.
|
// trigger() can restart the stream when it stalls. Stats persist across restarts.
|
||||||
|
|
@ -311,6 +316,15 @@ struct VimbaCameraSource::Impl {
|
||||||
for (auto& cam : cameras) e = std::max(e, getFloat(cam, "ExposureTime"));
|
for (auto& cam : cameras) e = std::max(e, getFloat(cam, "ExposureTime"));
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
// Longest frame period any camera runs at (the rate it reports), 0 if unknown.
|
||||||
|
double slowestFramePeriodMs() {
|
||||||
|
double p = 0;
|
||||||
|
for (auto& cam : cameras) {
|
||||||
|
const double fps = getFloat(cam, "AcquisitionFrameRate");
|
||||||
|
if (fps > 0) p = std::max(p, 1000.0 / fps);
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
// Reboot one camera, reopen it by id and re-apply its profile. See
|
// Reboot one camera, reopen it by id and re-apply its profile. See
|
||||||
// CameraStall.h for why this exists.
|
// CameraStall.h for why this exists.
|
||||||
|
|
@ -528,7 +542,15 @@ bool VimbaCameraSource::trigger() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newest <= impl_->last_captured_ts) {
|
if (newest <= impl_->last_captured_ts) {
|
||||||
LOG_TRACE_CAT(LogCat::Camera) << "stream stale; restarting acquisition";
|
// Nothing newer than the last frame kept - but with long exposures the
|
||||||
|
// next one may simply not be due yet. Restarting now would throw away the
|
||||||
|
// exposure in progress, so wait while that is still plausible.
|
||||||
|
const long long since_newest = nowMs() - newest;
|
||||||
|
if (onStaleFrame(since_newest, impl_->longestExposureUs(),
|
||||||
|
impl_->slowestFramePeriodMs()) == StallAction::Wait)
|
||||||
|
return false;
|
||||||
|
LOG_TRACE_CAT(LogCat::Camera) << "stream stale (" << since_newest
|
||||||
|
<< " ms since the newest frame); restarting acquisition";
|
||||||
for (auto& s : impl_->stats) s->restarts.fetch_add(1);
|
for (auto& s : impl_->stats) s->restarts.fetch_add(1);
|
||||||
try {
|
try {
|
||||||
impl_->stopAcquisition();
|
impl_->stopAcquisition();
|
||||||
|
|
@ -578,6 +600,7 @@ void VimbaCameraSource::beginExposureWatch() {
|
||||||
const size_t n = impl_->cameras.size();
|
const size_t n = impl_->cameras.size();
|
||||||
impl_->watch_ae.assign(n, {});
|
impl_->watch_ae.assign(n, {});
|
||||||
impl_->watch_from.assign(n, 0);
|
impl_->watch_from.assign(n, 0);
|
||||||
|
impl_->watch_unknown_logged = false;
|
||||||
for (size_t i = 0; i < n; ++i) {
|
for (size_t i = 0; i < n; ++i) {
|
||||||
impl_->watch_ae[i] = readAutoExposure(impl_->cameras[i]);
|
impl_->watch_ae[i] = readAutoExposure(impl_->cameras[i]);
|
||||||
std::lock_guard<std::mutex> lock(impl_->stats[i]->bright_mutex);
|
std::lock_guard<std::mutex> lock(impl_->stats[i]->bright_mutex);
|
||||||
|
|
@ -597,7 +620,15 @@ ExposureState VimbaCameraSource::exposureState() {
|
||||||
for (size_t i = 0; i < impl_->cameras.size(); ++i) {
|
for (size_t i = 0; i < impl_->cameras.size(); ++i) {
|
||||||
const AutoExposure& ae = impl_->watch_ae[i];
|
const AutoExposure& ae = impl_->watch_ae[i];
|
||||||
CamStats& st = *impl_->stats[i];
|
CamStats& st = *impl_->stats[i];
|
||||||
if (!ae.known || !st.measurable.load()) return ExposureState::Unknown;
|
if (!ae.known || !st.measurable.load()) {
|
||||||
|
if (!impl_->watch_unknown_logged) {
|
||||||
|
impl_->watch_unknown_logged = true;
|
||||||
|
LOG_DEBUG << "exposure cam" << i << ": cannot tell - "
|
||||||
|
<< (!ae.known ? "no IntensityControllerTarget/Tolerance readout"
|
||||||
|
: "pixel format is not 8-bit");
|
||||||
|
}
|
||||||
|
return ExposureState::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
long long since;
|
long long since;
|
||||||
double last, prev;
|
double last, prev;
|
||||||
|
|
@ -610,6 +641,7 @@ ExposureState VimbaCameraSource::exposureState() {
|
||||||
// Two fresh frames: the first after standstill may have been exposed partly
|
// Two fresh frames: the first after standstill may have been exposed partly
|
||||||
// during the slew.
|
// during the slew.
|
||||||
ExposureState s = ExposureState::Converging;
|
ExposureState s = ExposureState::Converging;
|
||||||
|
double exp = -1, gn = -1; // read only when off target
|
||||||
if (since >= 2) {
|
if (since >= 2) {
|
||||||
const double half = ae.tolerance + kSettleMarginPct;
|
const double half = ae.tolerance + kSettleMarginPct;
|
||||||
const bool dark = last < ae.target - half && prev < ae.target - half;
|
const bool dark = last < ae.target - half && prev < ae.target - half;
|
||||||
|
|
@ -620,8 +652,8 @@ ExposureState VimbaCameraSource::exposureState() {
|
||||||
s = ExposureState::Settled;
|
s = ExposureState::Settled;
|
||||||
} else if (dark || bright) {
|
} else if (dark || bright) {
|
||||||
const auto& cam = impl_->cameras[i];
|
const auto& cam = impl_->cameras[i];
|
||||||
const double exp = getFloat(cam, "ExposureTime");
|
exp = getFloat(cam, "ExposureTime");
|
||||||
const double gn = getFloat(cam, "Gain");
|
gn = getFloat(cam, "Gain");
|
||||||
const bool exp_pinned = !ae.exposure_auto ||
|
const bool exp_pinned = !ae.exposure_auto ||
|
||||||
(dark ? exp >= ae.exposure_max * 0.98 : exp <= ae.exposure_min * 1.02);
|
(dark ? exp >= ae.exposure_max * 0.98 : exp <= ae.exposure_min * 1.02);
|
||||||
const bool gain_pinned = !ae.gain_auto ||
|
const bool gain_pinned = !ae.gain_auto ||
|
||||||
|
|
@ -632,7 +664,13 @@ ExposureState VimbaCameraSource::exposureState() {
|
||||||
LOG_TRACE_CAT(LogCat::Camera)
|
LOG_TRACE_CAT(LogCat::Camera)
|
||||||
<< "exposure cam" << i << ": brightness " << last << " % (previous " << prev
|
<< "exposure cam" << i << ": brightness " << last << " % (previous " << prev
|
||||||
<< ") vs target " << ae.target << " +- " << ae.tolerance << ", " << since
|
<< ") vs target " << ae.target << " +- " << ae.tolerance << ", " << since
|
||||||
<< " frames since rest";
|
<< " frames since rest"
|
||||||
|
<< (exp >= 0 ? ", exposure " + std::to_string(exp) + " us (auto " +
|
||||||
|
std::to_string(ae.exposure_min) + ".." +
|
||||||
|
std::to_string(ae.exposure_max) + "), gain " +
|
||||||
|
std::to_string(gn) + " dB (auto max " +
|
||||||
|
std::to_string(ae.gain_max) + ")"
|
||||||
|
: std::string());
|
||||||
worst = std::min(worst, s);
|
worst = std::min(worst, s);
|
||||||
}
|
}
|
||||||
return worst;
|
return worst;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,17 @@ StallAction onNoFirstFrame(long long waited_ms, double exposure_us, int failed_r
|
||||||
return failed_restarts >= kRestartsBeforeReset ? StallAction::Reset : StallAction::Restart;
|
return failed_restarts >= kRestartsBeforeReset ? StallAction::Reset : StallAction::Restart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long long staleFrameLimitMs(double exposure_us, double frame_period_ms) {
|
||||||
|
const double exposure_ms = exposure_us > 0 ? exposure_us / 1000 : 0;
|
||||||
|
const double slowest = std::max(exposure_ms, frame_period_ms > 0 ? frame_period_ms : 0);
|
||||||
|
return std::max<long long>(2000, static_cast<long long>(2 * slowest) + 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
StallAction onStaleFrame(long long since_newest_ms, double exposure_us, double frame_period_ms) {
|
||||||
|
return since_newest_ms < staleFrameLimitMs(exposure_us, frame_period_ms) ? StallAction::Wait
|
||||||
|
: StallAction::Restart;
|
||||||
|
}
|
||||||
|
|
||||||
StallAction onRestartFailed(int failed_restarts) {
|
StallAction onRestartFailed(int failed_restarts) {
|
||||||
return failed_restarts >= kRestartsBeforeReset ? StallAction::Reset : StallAction::Restart;
|
return failed_restarts >= kRestartsBeforeReset ? StallAction::Reset : StallAction::Restart;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,3 +25,19 @@ TEST_CASE("a stale stream escalates to a reset once restarts stop working") {
|
||||||
CHECK(onRestartFailed(1) == StallAction::Restart);
|
CHECK(onRestartFailed(1) == StallAction::Restart);
|
||||||
CHECK(onRestartFailed(kRestartsBeforeReset) == StallAction::Reset);
|
CHECK(onRestartFailed(kRestartsBeforeReset) == StallAction::Reset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a slow next frame is not a stale stream") {
|
||||||
|
// Fast stream (5 ms exposure, 5 fps): the 2 s floor.
|
||||||
|
CHECK(staleFrameLimitMs(5000, 200) == 2000);
|
||||||
|
// Tower 111 at dusk: 9 s exposures, a frame every ~9 s.
|
||||||
|
CHECK(staleFrameLimitMs(9e6, 9000) == 2 * 9000 + 1000);
|
||||||
|
// A frame-rate cap slower than the exposure governs (night: 0.5 s at 0.215 fps).
|
||||||
|
CHECK(staleFrameLimitMs(5e5, 1000.0 / 0.215) == 2 * 4651 + 1000);
|
||||||
|
|
||||||
|
// Asked 8 s after the last frame of a 9 s exposure: wait for it, do not
|
||||||
|
// restart the stream and throw that exposure away (tower 111, 2026-10-04).
|
||||||
|
CHECK(onStaleFrame(8000, 9e6, 9000) == StallAction::Wait);
|
||||||
|
CHECK(onStaleFrame(20000, 9e6, 9000) == StallAction::Restart);
|
||||||
|
// A fast stream that has delivered nothing new for seconds is stale.
|
||||||
|
CHECK(onStaleFrame(2500, 5000, 200) == StallAction::Restart);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue