Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions Config/slicecamd.cfg.in
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,40 @@ FINE_ACQUIRE_BACKGROUND=(80 165 30 210)
#
FINE_ACQUIRE_MIN_SAMPLES=3

# FINE_ACQUIRE_CENTROID_SIGMA=<px>
# Gaussian weighting sigma (px) for the Step-4 centroid. The optimal value
# scales with the PSF width; default 1.5 was empirically most robust across
# SNR and seeing on real SCAM data (tools/eval_centroid_*.py). Larger favors
# faint stars in poor seeing; smaller favors sharp/bright stars.
#
FINE_ACQUIRE_CENTROID_SIGMA=1.5

# FINE_ACQUIRE_PREC=<arcsec>
# MAD scatter threshold per axis (arcsec). A correction is only commanded when
# the per-axis scatter of the collected samples is within this; a noisier batch
# is discarded and re-gathered rather than moving on an untrustworthy median.
#
FINE_ACQUIRE_PREC=0.4

# FINE_ACQUIRE_SETTLE_FRAMES=<n>
# <n> frames discarded after each telescope move to allow settling before
# centroid measurements resume.
#
FINE_ACQUIRE_SETTLE_FRAMES=2

# FINE_ACQUIRE_SETTLE_SEC=<sec>
# Seconds to wait after a commanded telescope move before evaluating the next
# frame. The move plus CCD readout takes time; acting on a frame grabbed mid-
# move/mid-settle gives a wrong correction. Time-based, independent of cadence.
#
FINE_ACQUIRE_SETTLE_SEC=4

# FINE_ACQUIRE_GAIN=<g>
# Proportional gain <g> = {0..1} applied to the commanded offset when the residual
# is at or below FINE_ACQUIRE_GAIN_THRESHOLD arcsec.
# Proportional gain <g> applied to the commanded offset. Use 1.0: a fine-tune
# step should apply the full measured offset -- deliberately under-correcting a
# known offset is never beneficial here.
#
FINE_ACQUIRE_GAIN=0.7
FINE_ACQUIRE_GAIN=1.0

# FINE_ACQUIRE_GAIN_LARGE=<g>
# Proportional gain <g> applied when the residual exceeds FINE_ACQUIRE_GAIN_THRESHOLD.
Expand Down
109 changes: 106 additions & 3 deletions slicecamd/slicecam_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ namespace Slicecam {

// start the state machine
this->fineacquire_state.reset();
this->fineacquire_state.last_frame_sig = 0; // fresh run: no prior frame to dedup against
this->fineacquire_state.consecutive_duplicate_frames = 0;
this->is_fineacquire_locked.store(false, std::memory_order_release);
this->is_fineacquire_running.store(true, std::memory_order_release);
this->is_autoexpose_running.store(false, std::memory_order_release); // fineacquire supersedes auto-exposure
Expand Down Expand Up @@ -269,6 +271,13 @@ namespace Slicecam {
void Interface::do_fineacquire() {
const char* function = "Slicecam::Interface::do_fineacquire";

// After commanding a TCS move, wait settle_sec before evaluating another
// frame. The move plus CCD readout takes time; acting on an image grabbed
// mid-move or mid-settle yields a wrong correction. Time-based so it does
// not depend on the frame cadence.
//
if ( std::chrono::steady_clock::now() < this->fineacquire_state.settle_until ) return;

// skip frames if we are waiting for the telescope to settle after a move
//
if (this->fineacquire_state.settle_frames > 0) {
Expand Down Expand Up @@ -302,6 +311,40 @@ namespace Slicecam {
return;
}

// Reject a duplicate frame. A commanded framegrab does not guarantee the
// image has refreshed; processing the same frame twice double-counts
// samples and can command a second move from a pre-move image (overshoot).
// Hash EVERY pixel (not a strided subsample): a sparse stride can miss the
// small star region near the aim point, so a fresh frame in which only the
// star moved could hash identically and be skipped forever, stalling the
// acquisition. FNV-1a over the whole frame is cheap at these sizes.
//
{
uint64_t sig = 1469598103934665603ULL; // FNV-1a offset basis
const auto* bytes = reinterpret_cast<const unsigned char*>( img_data.data() );
const size_t nbytes = img_data.size() * sizeof( float );
for ( size_t i = 0; i < nbytes; ++i ) {
sig = ( sig ^ static_cast<uint64_t>( bytes[i] ) ) * 1099511628211ULL;
}
if ( sig == this->fineacquire_state.last_frame_sig ) {
// Identical frame: skip it (don't double-count samples or move on a
// pre-move image). But don't skip forever -- this early return bypasses
// the no-detection timeout below, so a camera stuck on one frame (or a
// fixed emulated image) would otherwise acquire forever on a stale
// image. After many consecutive identical frames, give up.
if ( ++this->fineacquire_state.consecutive_duplicate_frames
>= 3 * this->fineacquire_state.max_samples ) {
logwrite( function, "ERROR camera frame not refreshing (repeated identical frames); "
"stopping fine acquisition" );
this->is_fineacquire_running.store( false, std::memory_order_release );
this->publish_status();
}
return;
}
this->fineacquire_state.last_frame_sig = sig;
this->fineacquire_state.consecutive_duplicate_frames = 0;
}

// find the star centroid near the aim point
//
Point centroid;
Expand All @@ -310,7 +353,8 @@ namespace Slicecam {
if ( Math::calculate_centroid( img_data, ncols, nrows,
this->fineacquire_state.bg_region,
this->fineacquire_state.aimpoint,
centroid, peak_raw, top10, peak_snr ) != NO_ERROR ) {
centroid, peak_raw, top10, peak_snr,
this->fineacquire_state.centroid_sigma ) != NO_ERROR ) {
const int max_failures = 3 * this->fineacquire_state.max_samples;

// ----- Auto-Adjust exposure time while finding centroid ---------------
Expand Down Expand Up @@ -514,6 +558,21 @@ namespace Slicecam {
}
}

// Never command a move from a noisy solution. We reach here either with the
// scatter within tolerance, or because we hit max_samples. In the latter
// case, if the scatter is still too high the median is not trustworthy:
// discard the batch and re-gather rather than applying a full-gain move to
// an unreliable position (which causes wrong/overshooting moves). This
// matches the reference engine, which refuses to move until precision is met.
//
if ( !scatter_ok ) {
logwrite( function, "fine acquisition: scatter too high to move safely after "
+std::to_string(n)+" samples (scatter=("+std::to_string(sig_dra)+","
+std::to_string(sig_ddec)+") > "+std::to_string(prec)+" arcsec); re-gathering" );
this->fineacquire_state.reset();
return;
}

// select gain: use gain_large when offset is well above the goal threshold
//
const double effective_gain = ( offset_arcsec > this->fineacquire_state.gain_threshold_arcsec )
Expand Down Expand Up @@ -542,9 +601,13 @@ namespace Slicecam {
return;
}

// reset samples and discard settle_count frames for telescope settling
// reset samples; ignore frames for settle_count frames AND for settle_sec
// seconds so the telescope move + readout completes before we measure again
this->fineacquire_state.reset();
this->fineacquire_state.settle_frames = this->fineacquire_state.settle_count;
this->fineacquire_state.settle_until = std::chrono::steady_clock::now()
+ std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::duration<double>( this->fineacquire_state.settle_sec ) );
}
/***** Slicecam::Interface::do_fineacquire **********************************/

Expand Down Expand Up @@ -670,7 +733,8 @@ namespace Slicecam {
const bool detected = ( Math::calculate_centroid( img_data, ncols, nrows,
this->fineacquire_state.bg_region,
this->default_aimpoint,
centroid, peak_raw, top10, peak_snr ) == NO_ERROR );
centroid, peak_raw, top10, peak_snr,
this->fineacquire_state.centroid_sigma ) == NO_ERROR );

if (detected) {
this->autoexpose_state.top10_window.push_back( top10 );
Expand Down Expand Up @@ -1371,6 +1435,45 @@ namespace Slicecam {
applied++;
}

if ( config.param[entry] == "FINE_ACQUIRE_CENTROID_SIGMA" ) {
try { this->fineacquire_state.centroid_sigma = std::stod( config.arg[entry] ); }
catch ( const std::exception &e ) {
message.str(""); message << "ERROR invalid FINE_ACQUIRE_CENTROID_SIGMA "
<< 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_PREC" ) {
try { this->fineacquire_state.prec_arcsec = std::stod( config.arg[entry] ); }
catch ( const std::exception &e ) {
message.str(""); message << "ERROR invalid FINE_ACQUIRE_PREC "
<< 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_SETTLE_SEC" ) {
try { this->fineacquire_state.settle_sec = std::stod( config.arg[entry] ); }
catch ( const std::exception &e ) {
message.str(""); message << "ERROR invalid FINE_ACQUIRE_SETTLE_SEC "
<< 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_EXPTIME_MIN" ) {
try { this->fineacquire_state.exptime_min = std::stod( config.arg[entry] ); }
catch ( const std::exception &e ) {
Expand Down
9 changes: 7 additions & 2 deletions slicecamd/slicecam_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,18 @@ namespace Slicecam {
std::vector<double> ddec_samp; ///< dDEC samples, degrees
int max_samples = 10; ///< samples before evaluating a move
int min_samples = 3; ///< minimum samples before scatter-gated early exit
double prec_arcsec = 0.1; ///< MAD scatter threshold per axis for early exit (arcsec)
double prec_arcsec = 0.4; ///< MAD scatter threshold per axis; never command a move unless scatter is within this (arcsec)
double goal_arcsec = 0.3; ///< convergence threshold, arcsec
double gain = 0.7; ///< gain applied when offset <= gain_threshold_arcsec
double gain = 1.0; ///< gain applied when offset <= gain_threshold_arcsec (full correction; never under-correct a known offset)
double gain_large = 1.0; ///< gain applied when offset > gain_threshold_arcsec
double gain_threshold_arcsec = 2.0; ///< offset above which gain_large is used
double centroid_sigma = 1.5; ///< Gaussian weighting sigma (px) for the centroid; empirically optimal across SNR/seeing
int settle_frames = 0; ///< countdown of frames to discard while telescope settles
int settle_count = 2; ///< configured: frames to discard after each move
double settle_sec = 4.0; ///< configured: seconds to wait after a TCS move before evaluating the next frame
std::chrono::steady_clock::time_point settle_until{}; ///< frames are ignored until this time (post-move settle)
uint64_t last_frame_sig = 0; ///< signature of the last processed frame, to reject duplicate (unrefreshed) frames
int consecutive_duplicate_frames = 0; ///< consecutive identical frames skipped; stop fine-acq if the camera is stuck
int consecutive_centroid_failures = 0; ///< counts consecutive centroid failures
// exposure compensation (shared by the reactive trim and, later, autoexpose)
double exptime_min = 0.1; ///< clamp: minimum auto-adjusted exposure (sec)
Expand Down
12 changes: 8 additions & 4 deletions slicecamd/slicecam_math.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ namespace Slicecam {
* Among all qualifying candidates, select the brightest.
*
* Step 4: refine to sub-pixel centroid via iterative Gaussian-
* windowed first-moment (centroid_sigma_pix = 2.0, 12 iterations,
* windowed first-moment (centroid_sigma_pix = centroid_sigma arg,
* 12 iterations,
* eps = 0.01 px).
*
* All pixel coordinates are FITS 1-based on input and output.
Expand All @@ -199,7 +200,8 @@ namespace Slicecam {
Point &centroid,
double &peak_raw,
double &top10_mean,
double &peak_snr ) {
double &peak_snr,
double centroid_sigma ) {
if ( image.empty() || ncols <= 0 || nrows <= 0 ) return ERROR;

// Convert 1-based inclusive ROI to 0-based, clamped
Expand Down Expand Up @@ -324,12 +326,14 @@ namespace Slicecam {
// --- Step 4: iterative Gaussian-windowed first-moment centroid ---
//
// centroid_halfwin = 4 (CF's --centroid-hw default)
// centroid_sigma_pix = 2.0 (CF's default, NOTE: different from filt_sigma)
// centroid_sigma_pix = centroid_sigma arg (configurable; default 1.5,
// empirically optimal on real SCAM data across SNR
// and seeing -- see tools/eval_centroid_*.py)
// centroid_maxiter = 12 (CF's default)
// centroid_eps_pix = 0.01
//
const int hw = 4;
const double s2 = 2.0 * 2.0; // centroid_sigma_pix^2
const double s2 = centroid_sigma * centroid_sigma; // centroid_sigma_pix^2

double cx = static_cast<double>( best_x );
double cy = static_cast<double>( best_y );
Expand Down
3 changes: 2 additions & 1 deletion slicecamd/slicecam_math.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ namespace Slicecam {
Point &centroid,
double &peak_raw, // raw ADU at source peak (saturation test)
double &top10_mean, // mean of top 10% bkg-subtracted pixels (scaling)
double &peak_snr ); // background-subtracted peak / background sigma
double &peak_snr, // background-subtracted peak / background sigma
double centroid_sigma = 1.5 ); // Gaussian weighting sigma (px) for the Step-4 centroid
/**
* @brief convert pixel coordinates to sky coordinates using WCS keys
*/
Expand Down
Loading