Quality Screening

Screen light frames for problems that ruin integrations but slip past conventional grading: occlusion from trees or a dome edge, small clouds, thin veils, errant light, and static glow. Every verdict can be rendered as an annotated diagnostic showing exactly which part of the frame drove the decision.

All detections are classical statistics. Thresholds were calibrated against real sessions (measured clean-frame envelopes across multiple nights and filters) and each detector carries regression tests pinning its behavior.

Why global metrics are not enough

Star count and HFR — what conventional graders use — barely move when a frame is partially ruined. Measured on a real session where a tree line progressively occluded the field:

The screening stack answers with signals that are local (per grid cell), photometric (flux ratios, not counts), and temporal (each cell compared to its own history) — with baselines that refuse to normalize slow-growing problems away.

The detection stack

SignalCatchesHow
Dead cellsOcclusion (trees, dome, dew shield) Fraction of 8×6 grid cells whose star density collapsed vs the frame's own median cell
TransparencyThin uniform veils Median flux ratio of stars matched against a per-sequence reference catalog; 0.7 = whole frame ~0.4 mag dimmer
Localized extinctionSmall clouds Per-cell flux ratios ÷ global transparency; a passing cloud is a coherent dip in one patch of stars
Star-share dropsSmall opaque clouds Each cell's share of the frame's stars vs that cell's own temporal median (Poisson-aware)
Background riseErrant light (headlights, flashlights) Per-cell background vs the cell's own history, after subtracting the frame's gradient (robust plane fit)
Background fallDark occluders, cloud shadow Same, downward: something blocking skyglow reads darker, not milky
Static glowCorner haze, lit occluder edges Cells brighter than the frame's own gradient model — catches problems present from a session's first frame, which temporal baselines can never see

The signals feed a sequence analyzer that scores every frame 0–1 relative to its session (same target, filter, and exposure; sessions split on 60-minute gaps) and classifies the likely cause. Verdicts: OK, WARN (recoverable or review-worthy — e.g. gradients that flat-ish processing can remove, glow that stacks into artifacts), REJECT (clouds and occlusion).

Quick start

# Screen a night of lights (no database needed)
psf-guard screen-fits "/path/to/2026-06-30/LIGHT"

# Render an annotated diagnostic PNG for every WARN/REJECT frame
psf-guard screen-fits "/path/to/LIGHT" --annotate /tmp/diagnostics

# Write [Auto] rejections into the Target Scheduler database
# (dry-run first; frames matched by filename AND capture timestamp)
psf-guard screen-fits "/path/to/LIGHT" --regrade-db my-db-slug --dry-run
psf-guard screen-fits "/path/to/LIGHT" --regrade-db my-db-slug

# Then archive the rejected files out of your stacking tree
psf-guard move-rejects --db my-db-slug

From the grader UI: open a target's Sequence view and press Scan Occlusion. The scan runs server-side in the background (progress shown live), results persist across restarts, and coverage badges and classifications appear on affected frames — "Select Clouded" bulk-selects flagged runs for rejection.

Reading the diagnostics

--annotate renders each flagged frame with the analysis grid overlaid. Cells are marked by the signal that fired; the caption strip carries the verdict, score, per-frame metrics, and the classifier's explanation.

MarkingMeaning
red filldead cell — star density collapsed (occlusion)
orange filllocalized extinction — stars dimmed (small cloud), labeled with the cell's flux ratio
magenta filltransient drop in the cell's share of stars
yellow bordertransient background rise (errant light)
blue bordertransient background fall (dark occluder / cloud shadow)
cyan fillstatic glow above the frame's own gradient model

Occlusion arriving

Occlusion onset: red cells trace the tree line
25% of the field's cells have lost their stars; the red region traces the visible out-of-focus occluder exactly. Transparency is 1.01 — the surviving field is photometrically perfect, which is why global metrics miss frames like this.

Heavy occlusion with a stray-lit edge

Heavy occlusion with a yellow-bordered stray-lit cell
Half the field is dead and a yellow border marks a cell whose background rose above its own temporal baseline — the occluder's stray-lit edge bleeding into a live cell.

The advancing frontier

Blue cells darker than history, yellow lit fringe
Late in the same session: blue borders mark cells reading darker than their own history (the dark occluder blocking skyglow as it advances), yellow marks its lit fringe.

Thin cloud veil — pure photometry

Clean frame: 2973 stars, transparency 1.05
Clean frame: 2,973 stars, transparency 1.05.
Veiled frame: 1417 stars, transparency 0.63, REJECT
Same field 13 minutes later: 1,417 stars, transparency 0.63 (~0.5 mag of uniform extinction). REJECT.

No cell is tinted because nothing is locally wrong — only the matched-star flux ratios see it. This frame was Accepted by conventional grading.

Static corner glow

Cyan cells sitting on corner haze at 4.7% above the gradient plane
A haze present from the session's first frame — every temporal detector is structurally blind to it. The static glow signal compares each cell against the frame's own gradient model instead: the cyan cells sit exactly on the haze at 4.7% above the plane.

Tuning

Defaults were calibrated against measured clean-frame envelopes (42+ frames, 4 nights, multiple filters). The main knobs:

KnobDefaultNotes
--min-score0.35Composite score below which a frame is rejected
--dead-cell-rise0.08Occlusion onset sensitivity; clean-frame jitter is ≤0.04, so 0.08 is a 2× margin
--session-gap60 minSplits sequences into sessions
glow threshold2.5% of sky and >30 ADUThe ADU floor keeps real narrowband nebulosity (measured 19–22 ADU) from false-flagging; true haze measured 48–103 ADU. Rig-specific.
transparency threshold0.80Global veil rejection level

Safety properties

Limitations

The full technical document lives in the repository: docs/SCREENING.md.