diff --git a/Config/slicecamd.cfg.in b/Config/slicecamd.cfg.in index a7eb0af4..0e977379 100644 --- a/Config/slicecamd.cfg.in +++ b/Config/slicecamd.cfg.in @@ -149,6 +149,15 @@ FINE_ACQUIRE_SATURATION=55000 FINE_ACQUIRE_COUNTS_BRIGHT=10000 FINE_ACQUIRE_COUNTS_BRIGHT_GOAL=2500 +# FINE_ACQUIRE_MIN_SNR= +# Peak signal-to-noise (background-subtracted peak / background sigma) at or above +# which a source is considered well enough defined to centroid reliably. Above this +# fine-acquire and autoexpose stop RAISING the exposure even if the source is still +# below FINE_ACQUIRE_COUNTS_FAINT. +# Must be >=2 if set, or leave unset to disable SNR requirement and use only count band. +# +FINE_ACQUIRE_MIN_SNR=3 + # FINE_ACQUIRE_AUTOEXPOSE_WINDOW= # Number of frames the pre-acquisition auto-exposure averages before each # decision. diff --git a/slicecamd/slicecam_interface.cpp b/slicecamd/slicecam_interface.cpp index 5819aa77..01604a4b 100644 --- a/slicecamd/slicecam_interface.cpp +++ b/slicecamd/slicecam_interface.cpp @@ -305,12 +305,12 @@ namespace Slicecam { // find the star centroid near the aim point // Point centroid; - double peak_raw = 0.0, top10 = 0.0; + double peak_raw = 0.0, top10 = 0.0, peak_snr = 0.0; if ( Math::calculate_centroid( img_data, ncols, nrows, this->fineacquire_state.bg_region, this->fineacquire_state.aimpoint, - centroid, peak_raw, top10 ) != NO_ERROR ) { + centroid, peak_raw, top10, peak_snr ) != NO_ERROR ) { const int max_failures = 3 * this->fineacquire_state.max_samples; // ----- Auto-Adjust exposure time while finding centroid --------------- @@ -405,6 +405,7 @@ namespace Slicecam { this->fineacquire_state.dra_samp.push_back( offsets.first ); this->fineacquire_state.ddec_samp.push_back( offsets.second ); this->fineacquire_state.top10_samp.push_back( top10 ); + this->fineacquire_state.snr_samp.push_back( peak_snr ); const int n = static_cast( this->fineacquire_state.dra_samp.size() ); const int max_samples = this->fineacquire_state.max_samples; @@ -481,11 +482,25 @@ namespace Slicecam { std::sort( sorted_top10.begin(), sorted_top10.end() ); const double median_top10 = sorted_top10[ sorted_top10.size() / 2 ]; + std::vector sorted_snr = this->fineacquire_state.snr_samp; + std::sort( sorted_snr.begin(), sorted_snr.end() ); + const double median_snr = sorted_snr.empty() ? 0.0 : sorted_snr[ sorted_snr.size() / 2 ]; + const double cur = this->camera.andor.empty() ? 0.0 : this->camera.andor.begin()->second->camera_info.exptime; - const double new_exptime = this->banded_exptime( cur, median_top10 ); + double new_exptime = this->banded_exptime( cur, median_top10 ); + + // meeting SNR requirement overrides count requirement + // + if ( new_exptime > cur && std::isfinite( this->fineacquire_state.min_snr ) && + median_snr >= this->fineacquire_state.min_snr ) { + logwrite( function, "exptime raise vetoed: peak SNR "+std::to_string(median_snr) + +" >= "+std::to_string(this->fineacquire_state.min_snr)+" (adequate for centroid)" ); + new_exptime = cur; + } // banded_exptime returns cur when in band; only act on a material change + // if ( std::abs( new_exptime - cur ) >= 0.02 ) { logwrite( function, "exptime trim "+std::to_string(cur)+" -> "+std::to_string(new_exptime) +" s (top10="+std::to_string(median_top10) @@ -651,15 +666,16 @@ namespace Slicecam { // is fine -- it just means "no source in this frame") // Point centroid; - double peak_raw = 0.0, top10 = 0.0; + double peak_raw = 0.0, top10 = 0.0, peak_snr = 0.0; const bool detected = ( Math::calculate_centroid( img_data, ncols, nrows, this->fineacquire_state.bg_region, this->default_aimpoint, - centroid, peak_raw, top10 ) == NO_ERROR ); + centroid, peak_raw, top10, peak_snr ) == NO_ERROR ); if (detected) { this->autoexpose_state.top10_window.push_back( top10 ); if (peak_raw > this->autoexpose_state.max_peak_raw) this->autoexpose_state.max_peak_raw = peak_raw; + if (peak_snr > this->autoexpose_state.max_snr) this->autoexpose_state.max_snr = peak_snr; this->autoexpose_state.detect_count++; } @@ -695,7 +711,11 @@ namespace Slicecam { const double estimate = window[idx]; this->autoexpose_state.no_detect_count = 0; - const double new_exptime = this->banded_exptime( cur, estimate ); + double new_exptime = this->banded_exptime( cur, estimate ); + if ( new_exptime > cur && std::isfinite( this->fineacquire_state.min_snr ) && + this->autoexpose_state.max_snr >= this->fineacquire_state.min_snr ) { + new_exptime = cur; // already well above noise; do not raise + } if (std::abs(new_exptime - cur) >= 0.02) { logwrite(function, "exptime "+std::to_string(cur)+" -> "+std::to_string(new_exptime) +" s (top10="+std::to_string(estimate) @@ -1416,6 +1436,19 @@ namespace Slicecam { applied++; } + if ( config.param[entry] == "FINE_ACQUIRE_MIN_SNR" ) { + try { this->fineacquire_state.min_snr = std::stod( config.arg[entry] ); } + catch ( const std::exception &e ) { + message.str(""); message << "ERROR invalid FINE_ACQUIRE_MIN_SNR " + << config.arg[entry] << ": " << e.what(); + logwrite( function, message.str() ); + return ERROR; + } + message.str(""); message << "SLICECAMD:config:" << config.param[entry] << "=" << config.arg[entry]; + logwrite( function, message.str() ); + applied++; + } + if ( config.param[entry] == "FINE_ACQUIRE_COUNTS_BRIGHT" ) { try { this->fineacquire_state.counts_bright = std::stod( config.arg[entry] ); } catch ( const std::exception &e ) { @@ -1515,6 +1548,15 @@ namespace Slicecam { } } + // min_snr, when set, must be at least 2 + // NAN (unset) disables SNR override + // + if ( std::isfinite( this->fineacquire_state.min_snr ) && + this->fineacquire_state.min_snr < 2.0 ) { + logwrite( function, "ERROR FINE_ACQUIRE_MIN_SNR must be >= 2 when set" ); + return ERROR; + } + message.str(""); message << "applied " << applied << " configuration lines to the slicecam interface"; logwrite(function, message.str()); diff --git a/slicecamd/slicecam_interface.h b/slicecamd/slicecam_interface.h index f81effdf..f8d3920f 100644 --- a/slicecamd/slicecam_interface.h +++ b/slicecamd/slicecam_interface.h @@ -103,10 +103,12 @@ namespace Slicecam { double counts_faint_goal = NAN; ///< faint-mode brightness goal double counts_bright = NAN; ///< above this lower toward counts_bright_goal double counts_bright_goal = NAN; ///< bright-mode brightness goal - int autoexpose_window = 2; ///< frames per pre-acquisition auto-exposure decision - std::vector top10_samp; ///< per-frame top-10%-mean brightness samples, parallel to dra_samp + double min_snr = NAN; ///< peak SNR at/above which faint raises are vetoed; NAN disables + int autoexpose_window = 2; ///< frames per pre-acquisition auto-exposure decision + std::vector top10_samp; ///< per-frame top-10%-mean brightness samples, parallel to dra_samp + std::vector snr_samp; ///< per-frame peak-SNR samples, parallel to top10_samp - void reset() { dra_samp.clear(); ddec_samp.clear(); top10_samp.clear(); + void reset() { dra_samp.clear(); ddec_samp.clear(); top10_samp.clear(); snr_samp.clear(); settle_frames = 0; consecutive_centroid_failures = 0; } bool is_valid() const noexcept { return !which.empty() && aimpoint.is_valid() && bg_region.is_valid(); @@ -148,11 +150,12 @@ namespace Slicecam { struct AutoExpState { std::vector top10_window; ///< per-frame top-10%-mean values in the window double max_peak_raw = 0.0; ///< max raw peak in the window (saturation) + double max_snr = 0.0; ///< max peak-SNR in the window int detect_count = 0; ///< detections in the window int frames_seen = 0; ///< frames accumulated in the window int no_detect_count = 0; ///< consecutive empty windows int settle_frames = 0; ///< skip stale frames after an exptime change - void start_window() { top10_window.clear(); max_peak_raw = 0.0; detect_count = 0; frames_seen = 0; } + void start_window() { top10_window.clear(); max_peak_raw = 0.0; max_snr = 0.0; detect_count = 0; frames_seen = 0; } void reset() { start_window(); no_detect_count = 0; settle_frames = 0; } } autoexpose_state; diff --git a/slicecamd/slicecam_math.cpp b/slicecamd/slicecam_math.cpp index 1393f6c7..9ee11908 100644 --- a/slicecamd/slicecam_math.cpp +++ b/slicecamd/slicecam_math.cpp @@ -198,7 +198,8 @@ namespace Slicecam { Point aimpoint, Point ¢roid, double &peak_raw, - double &top10_mean ) { + double &top10_mean, + double &peak_snr ) { if ( image.empty() || ncols <= 0 || nrows <= 0 ) return ERROR; // Convert 1-based inclusive ROI to 0-based, clamped @@ -379,6 +380,10 @@ namespace Slicecam { // sensitive to seeing and intra-pixel position) that also tracks // exposure time linearly, which is what the scaling relies on. peak_raw = best_val + bkg; + + // peak_snr is background-subtracted peak over the background noise sigma. + // + peak_snr = ( sigma > 0.0 ) ? best_val / sigma : 0.0; { const long bxp = static_cast( std::floor( cx ) ); const long byp = static_cast( std::floor( cy ) ); diff --git a/slicecamd/slicecam_math.h b/slicecamd/slicecam_math.h index 9c94fa97..6c5cea65 100644 --- a/slicecamd/slicecam_math.h +++ b/slicecamd/slicecam_math.h @@ -56,7 +56,8 @@ namespace Slicecam { Point aimpoint, Point ¢roid, double &peak_raw, // raw ADU at source peak (saturation test) - double &top10_mean ); // mean of top 10% bkg-subtracted pixels (scaling) + double &top10_mean, // mean of top 10% bkg-subtracted pixels (scaling) + double &peak_snr ); // background-subtracted peak / background sigma /** * @brief convert pixel coordinates to sky coordinates using WCS keys */