Subsample instead of dropping frames with too many star candidates; improve star extraction robustness#918
Subsample instead of dropping frames with too many star candidates; improve star extraction robustness#918Cybis320 wants to merge 16 commits into
Conversation
- On candidate overflow, keep a spatially stratified sample of the most prominent candidates (white top-hat ranking, equal quota per image tile) instead of dropping the whole frame. Frames under the limit are unchanged. - Log the max_global_intensity gate at both call sites and raise its default from 150 to 230 (frames at median ~190-225 still hold dozens of real stars). - Skip too-bright chunks in extractStarsImgHandle instead of aborting the whole image handle and discarding stars from all other chunks. - SkyFit2 star detection tab: add Max Global Intensity slider, split the max stars budget into a session-only SkyFit value and a Config value written by Save Config (400 recommended), and add a Reset to Defaults button. - Update the star detection help topic accordingly.
- Split the star detection sliders into Station Config (written by Save Config) and SkyFit Session Only group boxes - Move gamma to the Station Config group and have Save Config write it to the [Capture] config section; track gamma in the modified-config check - Tighten group box margins and shorten labels so nothing clips at the fixed tab width - Add a checkable Auto Levels button to the Levels tab, kept in sync with the Ctrl+A shortcut via a new sigAutoLevelsToggled signal - Update the star detection and levels help topics
- Both max stars sliders move in 100-star increments (drag snapping, arrow keys step 100, page step 500, minimum raised to 100) - Wrap the form-style tabs (Fit Parameters, Station, Star Detection, Mask, Settings, Debruijn) in scroll areas so short windows scroll the content instead of compressing it; Levels and Help stay unwrapped - Add tabWidgetFor()/tabIndexOf() resolvers and route identity-based tab lookups through them
The Help tab rework (7b8da60) collapsed the three-state F1 cycle into a show/hide toggle and incidentally flipped the panel's startup state back to visible, reverting 5d7064b. Restore hidden-by-default, but keep the panel force-shown as the empty-state hint when no data is loaded (and hide it again once data arrives).
|
Thanks Luc — this is a well-motivated and carefully validated PR. I did an in-depth review of the diff plus an independent empirical check of the core algorithm, and audited every downstream CALSTARS consumer. Summary below so you know where to pick things up. What I verified (independent tests, synthetic star fields, old vs. new code)
The subsampler itself is sound — I could not break it. The items below are all in the surrounding GUI/config plumbing and in the framing of downstream effects. Requested changes1. 2. The config max-stars slider snaps/clamps, then Save Config writes the altered value. Points for discussion (no code change strictly required)3. "Science impact: structurally unaffected" holds for catalog-matched paths, but several consumers act on raw CALSTARS counts and do react to junk entries.
4. Default rollout reach. The Minor
Overall: the core change is a clear improvement and the validation in the PR description reproduces. Once items 1 and 2 are addressed (both small fixes), this is good to go from my side. |
Review fixes for #918 (items 1 and 2): - Reset to Defaults no longer resets gamma to 1.0 - it is a hardware property of the camera, and resetting it silently corrupted photometry the moment Save Config wrote it to the station config. Gamma and the config max stars now return to the loaded station config values; only the tuning sliders return to the recommended defaults. - Programmatic slider seeding bypasses the snap-to-100 handlers, so a config value like max_stars=150 loads exactly instead of snapping to 200, showing as an unsaved modification with zero user input, and being written back snapped. User motion still snaps. Out-of-range config values extend the slider range instead of clamping.
Review follow-ups for #918 (item 3 cheap fixes and the docstring note): - autoCheckFit seeds its NN alignment from the FF file with the most SNR-filtered stars instead of the most raw entries, so a junk-flooded frame cannot win the seed selection (falls back to the raw count for older CALSTARS files without an SNR column). - The flux sensor FWHM median is computed over SNR-filtered detections for the same reason, with the same fallback. - max_global_intensity docstrings updated: the gate compares the median (not mean) image intensity, default 230.
Empirical check of the previous commit's flux change: on a frame that is 94% junk (866 detections, 54 catalog-matched), the raw FWHM median differs from the matched-only median by just 0.07 px - junk that survives the PSF fit has star-like FWHM by construction, and the median is robust to it exactly as designed. Meanwhile the SNR >= 5 filter shifts the median by +0.43 px on a perfectly normal night (standard-settings detections have median SNR ~3.5, so the cut keeps only the brightest third), which would have changed flux results network-wide. The SNR-filtered seed selection in autoCheckFit stands - for that consumer the same measurement confirms it: junk sits at SNR ~1.3, real stars at ~8, the ranking is unaffected on clean nights and stops junk-flooded frames from winning on bad ones.
The cloud-detection ratio compares catalog-matched detections against a predicted star count derived from the photometric zero-point - a model that does not know the extractor never returns more than config.max_stars detections. On star-rich nights with sensitive cameras the prediction can exceed the cap severalfold (at detector LM ~8, a 40x22 deg FOV holds ~900 predicted stars against a 400-star cap), so a perfectly clear sky reads as cloudy (matched ~ cap, ratio < 0.5). Before the subsampler such frames were dropped entirely and always read as cloudy; now they deliver a full cap of matches, so the ratio must compare against what detection is actually allowed to deliver: min(predicted, max_stars). Also guards a division by zero when the prediction is empty.
|
Thanks for the deep review, Denis, and especially for the independent empirical checks - the flood-recovery and uniformity verifications are exactly the tests I'd have wanted a second pair of eyes on. Both requested changes are addressed, plus the cheap fixes from the discussion items: 1. Gamma / Reset to Defaults - fixed. Reset to Defaults now returns the station-bound values to the loaded config instead of global defaults: gamma goes back to the station's configured value (it's a hardware property, as you said), and the config max stars likewise. Only the tuning sliders reset to the recommended defaults. 2. Slider snap on load - fixed. Programmatic seeding now bypasses the snap-to-100 handlers, so 3. Raw-count consumers - measured both suggestions before/after; one is in, one turned out to be counterproductive:
4. Rollout reach - correct. Deployed stations keep their configured Docstrings at Pushed as four commits on top of the branch (including the measured revert of the flux FWHM filter and the clear-sky prediction saturation). |
Since the subsampler, exceeding max_stars keeps the best-distributed most prominent subset instead of dropping the frame - so neither max-stars value is a detection-quality knob anymore, and tuning or seeding them from sky content no longer makes sense: - The SkyFit session budget defaults to 2000 and is no longer seeded from the station config, whose value is the Pi's pipeline budget - seeding dragged the deep calibration default down on every folder load. - Tune no longer sets max_stars from the candidate count and the seasonal star variation sweep (both fossils from when overflowing the cap lost the frame); it tunes what remains quality-relevant - intensity threshold and segment radius. The now-unused seasonal variation helper is removed. - Reset to Defaults returns the session budget to 2000 and the config budget to the station's configured value, as before.
The clear-sky ratio saturates the prediction at max_stars (the extractor never returns more), but the plot still showed the raw prediction with a 'Matched/Predicted stars' ratio above it - on star-rich nights the two panels disagreed (e.g. ratio 0.80 plotted while the panels showed 320/870). Plot the capped prediction the ratio actually uses (only when the cap binds) and note the capping in the ratio axis label.
…ames The clear-sky ratio saturated the prediction at max_stars, but the cap counts candidates while the numerator counts catalog-matched survivors of the PSF fit - a clear sky on a cap-bound night plateaued at ~0.75 rather than 1, eating into the threshold margin on cameras with weaker matching. Since observing periods are computed after the night, the deficit is measurable from the night itself: the median matched/predicted over frames where the cap does not bind and the sky is already clearly clear. The expectation becomes min(predicted, deficit*max_stars) - continuous in the prediction, exact historical semantics below the effective ceiling, and a clear capped sky now reads ~1.0 (measured on a real cap-bound starry night: 0.75-0.80 -> ~1.0, cloudy frames unaffected at ~0.2). Clamped to [0.5, 1] so a contaminated sample cannot lift cloudy frames past the threshold, and inert (previous behavior) on nights with no usable calibration sample. The observing-periods plot shows the expectation series it actually uses.
The ratio is now 'fraction of the clear-sky expectation' rather than a raw matched/predicted fraction, and can legitimately sit slightly above 1 on cap-bound frames (the deficit is calibrated on uncapped frames, whose fainter stars underperform the bright capped subset). A dotted reference at 1.0 makes the reading direct: near the green line = clear, under the red line = cloudy.
Legend and labels stick to the three nouns - matched, predicted, expected stars - with the 1.0 reference reading 'Matched = Expected'. The cap and calibrated deficit stay in the log, not the legend.
A horizontal reference at 1 asserts a calibrated expectation that the prediction model does not deliver: on a second live station the uncapped matched/predicted runs 1.2-1.5 early night and drifts to 0.9 - the limiting-magnitude prediction is biased low for that camera, and time-varyingly so. The deficit calibration correctly refuses to inflate expectations (clamped at 1), so measurements legitimately sit far above the line and it reads as broken. The threshold line is the decision boundary and the bottom panel carries the counts; the reference line only misleads.
predictStarNumberInFOV filters predicted stars through the mask, but only the nightly pipeline caller passed one - the manual Flux CLI and FluxBatch ran the prediction unmasked, counting stars behind obstructions that matched stars can never contain. On heavily masked cameras this biases the clear-sky ratio low in batch flux runs, rejecting intervals that the same night passes on-station.
Motivation
Data from the MQTT health monitor dashboard shows that many stations regularly drop FF files from star extraction. The drops are disproportionately the best FF files of the night, since clear, dark skies produce the most star candidates and trip the overflow limit.
Problem
When the number of star candidates on a frame exceeds
max_stars,extractStars()drops the entire frame ("Too many candidate stars to process!") and returns zero stars. This triggers in two very different situations:A side effect of the old behavior:
intensity_thresholdhad to be tuned high enough to keep candidate counts undermax_stars, or frames would be dropped — so sensitivity and processing cost could not be set independently.Two related issues:
max_global_intensitygate discards frames silently (no log trace, indistinguishable from a genuinely starless frame), and its default of 150 rejects frames that measurably still hold dozens of stars.extractStarsImgHandle(video/image-dir processing), a single too-bright chunk aborts all chunks, discarding stars already extracted and disabling meteor detection for the whole file.Changes
intensity_thresholdcan be set purely for desired sensitivity andmax_starspurely as the compute budget — a low threshold no longer risks dropping frames, it just engages the subsampler.max_global_intensitygate is logged at both call sites (median and cutoff values), and the default is raised 150 → 230. The gate operates on the flat-corrected median; frames at median 190–225 were measured to still hold 20–45 stars with 75–95% frame-to-frame repeatability.extractStarsImgHandle— other chunks keep their stars.Note
extractStars()no longer returnsFalse; the callers'status == Falsebranches remain but are now dead paths.Validation (real FF files, several stations)
max_stars400)max_stars— same worst case as a legitimately rich frame todayScience impact
Per-star measurements are unchanged (same PSF fit). Junk candidates can't match the star catalog, so recalibration, photometry, and flux cloud detection are structurally unaffected. The net effect is additive: frames that previously contributed zero stars now contribute vetted samples — including the best dark-sky frames on sensitive cameras — so expect a completeness step at deployment (more usable observing time in long time series).