Add waxing/waning crescent phases; fix phase-shadow double-application
moon_detect.py - detect_moon() accepts saturated_threshold / min_roundness / max_halo_ratio so crescent phases (dim, non-circular arc) can be detected with relaxed thresholds without changing defaults for full/quarter detection. moon_phase.py - crescent_times_in_range(): 2-hour scan returning the UTC moment illumination crosses a target fraction (default 28%) on the waxing or waning side. Used for scheduling just like phase_events_in_range() for named phases. moon_composite.py - Remove _apply_phase_shadow() from composite_full_moon. NASA SVS Dial-a-Moon renders already include the correct terminator, libration and earthshine for the exact hour; applying the shadow on top double-darkened the unlit limb on every non-full phase. The function stays available for callers that need it. moon_phase_monthly.py - Add waxing-crescent (🌒) and waning-crescent (🌘) to PHASE_SPEC. Timing via crescent_times_in_range(); scheduling identical to other phases. - CRESCENT_* tuning constants (all overridable from sky-cam.conf): MOON_CRESCENT_TARGET_ILLUM default 0.28 MOON_CRESCENT_MIN_QUALITY default 0.35 MOON_CRESCENT_SATURATED_THR default 180 MOON_CRESCENT_MIN_ROUNDNESS default 0.15 MOON_CRESCENT_MAX_HALO_RATIO default 12.0 - run_phase() and auto_run() branch on spec['crescent'] to use crescent timing and detection params. - Atmospheric background naturally captures twilight colours (dawn for waning crescent, dusk/dawn for waxing) from the real east frame. https://claude.ai/code/session_01HuJ83KvMvshiY6HxJbtMsc
This commit is contained in:
+65
-9
@@ -87,6 +87,14 @@ REQUIRE_EAST_VERIFY = CONF.get('MOON_REQUIRE_EAST_VERIFY', 'true').lower() != 'f
|
||||
|
||||
DARK_START_MIN = int(CONF.get('MOON_DARK_START_MIN', 30))
|
||||
|
||||
# Crescent detection uses relaxed thresholds: a crescent arc scores low on
|
||||
# roundness and is dimmer than a full disk.
|
||||
CRESCENT_TARGET_ILLUM = float(CONF.get('MOON_CRESCENT_TARGET_ILLUM', 0.28))
|
||||
CRESCENT_MIN_QUALITY = float(CONF.get('MOON_CRESCENT_MIN_QUALITY', 0.35))
|
||||
CRESCENT_SATURATED_THR = int(CONF.get('MOON_CRESCENT_SATURATED_THR', 180))
|
||||
CRESCENT_MIN_ROUNDNESS = float(CONF.get('MOON_CRESCENT_MIN_ROUNDNESS', 0.15))
|
||||
CRESCENT_MAX_HALO_RATIO = float(CONF.get('MOON_CRESCENT_MAX_HALO_RATIO', 12.0))
|
||||
|
||||
|
||||
PHASE_SPEC = {
|
||||
'full': {
|
||||
@@ -110,6 +118,28 @@ PHASE_SPEC = {
|
||||
'post_delay': int(CONF.get('MOON_QUARTER_POST_DELAY_DAYS', 1)),
|
||||
'enabled_key': 'MOON_THIRD_QUARTER_ENABLED',
|
||||
},
|
||||
# Crescents have no fixed skyfield phase index — timing is computed via
|
||||
# crescent_times_in_range() at the configured illumination target (~28%).
|
||||
# Detection uses relaxed thresholds since a crescent arc is non-circular
|
||||
# and dimmer than a full disk. The atmospheric background naturally picks
|
||||
# up twilight colours from the real east frame (dawn for waning, dusk/dawn
|
||||
# depending on the month for waxing).
|
||||
'waxing-crescent': {
|
||||
'crescent': True,
|
||||
'waxing': True,
|
||||
'label': 'Waxing Crescent',
|
||||
'emoji': '🌒',
|
||||
'post_delay': int(CONF.get('MOON_CRESCENT_POST_DELAY_DAYS', 1)),
|
||||
'enabled_key': 'MOON_WAXING_CRESCENT_ENABLED',
|
||||
},
|
||||
'waning-crescent': {
|
||||
'crescent': True,
|
||||
'waxing': False,
|
||||
'label': 'Waning Crescent',
|
||||
'emoji': '🌘',
|
||||
'post_delay': int(CONF.get('MOON_CRESCENT_POST_DELAY_DAYS', 1)),
|
||||
'enabled_key': 'MOON_WANING_CRESCENT_ENABLED',
|
||||
},
|
||||
}
|
||||
|
||||
_FRAME_RE = re.compile(r'^(\d{2})-(\d{2})-(\d{2})\.jpg$')
|
||||
@@ -281,12 +311,21 @@ def run_phase(phase: str, target_utc: datetime | None, cam: str,
|
||||
import moon_phase
|
||||
from moon_detect import detect_moon
|
||||
|
||||
is_crescent = spec.get('crescent', False)
|
||||
|
||||
if target_utc is None:
|
||||
now = datetime.now(timezone.utc)
|
||||
events = moon_phase.phase_events_in_range(
|
||||
now - timedelta(days=45), now, spec['index'])
|
||||
if is_crescent:
|
||||
events = moon_phase.crescent_times_in_range(
|
||||
now - timedelta(days=35), now,
|
||||
target_illum=CRESCENT_TARGET_ILLUM,
|
||||
waxing_side=spec['waxing'],
|
||||
)
|
||||
else:
|
||||
events = moon_phase.phase_events_in_range(
|
||||
now - timedelta(days=45), now, spec['index'])
|
||||
if not events:
|
||||
print(f'no recent {phase} found in past 45 days', file=sys.stderr)
|
||||
print(f'no recent {phase} found', file=sys.stderr)
|
||||
return 1
|
||||
target_utc = events[-1]
|
||||
|
||||
@@ -336,12 +375,21 @@ def run_phase(phase: str, target_utc: datetime | None, cam: str,
|
||||
# is the one temporally closest to the moon being precisely full/quarter.
|
||||
dark_frames.sort(key=lambda x: abs(x[1] - target_utc))
|
||||
|
||||
# Crescent arcs are non-circular and dimmer — use relaxed detection params.
|
||||
det_kwargs = (
|
||||
dict(saturated_threshold=CRESCENT_SATURATED_THR,
|
||||
min_roundness=CRESCENT_MIN_ROUNDNESS,
|
||||
max_halo_ratio=CRESCENT_MAX_HALO_RATIO)
|
||||
if is_crescent else {}
|
||||
)
|
||||
min_q = CRESCENT_MIN_QUALITY if is_crescent else MIN_QUALITY
|
||||
|
||||
best_frame: str | None = None
|
||||
best_det = None
|
||||
best_dt: datetime | None = None
|
||||
for fpath, fdt in dark_frames:
|
||||
det = detect_moon(fpath)
|
||||
if det is not None and det.quality >= MIN_QUALITY:
|
||||
det = detect_moon(fpath, **det_kwargs)
|
||||
if det is not None and det.quality >= min_q:
|
||||
best_frame = fpath
|
||||
best_det = det
|
||||
best_dt = fdt
|
||||
@@ -451,16 +499,24 @@ def auto_run(cam: str, dry_run: bool, no_upload: bool) -> int:
|
||||
"""Daily check: run any phase whose post-day equals today (UTC)."""
|
||||
import moon_phase
|
||||
today_utc = datetime.now(timezone.utc).date()
|
||||
window_start = datetime.combine(
|
||||
today_utc - timedelta(days=45), datetime.min.time(), tzinfo=timezone.utc)
|
||||
now = datetime.now(timezone.utc)
|
||||
ran_any = False
|
||||
rc = 0
|
||||
for phase, spec in PHASE_SPEC.items():
|
||||
if CONF.get(spec['enabled_key'], 'true').lower() == 'false':
|
||||
print(f'-- {phase}: {spec["enabled_key"]}=false → skip')
|
||||
continue
|
||||
events = moon_phase.phase_events_in_range(
|
||||
datetime.combine(today_utc - timedelta(days=45), datetime.min.time(), tzinfo=timezone.utc),
|
||||
datetime.now(timezone.utc),
|
||||
spec['index'])
|
||||
if spec.get('crescent'):
|
||||
events = moon_phase.crescent_times_in_range(
|
||||
window_start, now,
|
||||
target_illum=CRESCENT_TARGET_ILLUM,
|
||||
waxing_side=spec['waxing'],
|
||||
)
|
||||
else:
|
||||
events = moon_phase.phase_events_in_range(
|
||||
window_start, now, spec['index'])
|
||||
if not events:
|
||||
continue
|
||||
last_event = events[-1]
|
||||
|
||||
Reference in New Issue
Block a user