Simplify moon phase: binary go/no-go, parallactic angle rotation, no atmosphere overlay
- moon_composite.py: remove _make_atmosphere_layer() and all atmosphere parameters from render_phase_closeup(); add when_utc parameter and apply parallactic angle rotation so the NASA disk is oriented to match east's sky - moon_phase_monthly.py: change obs time default to 22:00 (aligns with NASA hourly renders); scan east frames in 22:00-23:00 window; binary go/no-go (quality >= MIN_QUALITY = post, no clear shot = skip entirely); remove _atmosphere_opacity_from_quality(), ATMOSPHERE_BLUR, CLOUDY_POST_ENABLED - sky-cam.conf: update MOON_OBS_TIME_LOCAL to 22:00, remove MOON_OBS_WINDOW_MIN, remove the atmospheric overlay and cloudy fallback sections and their config variables - test_moon_composite.py: single clean test — fetch NASA for 22:00 UTC on 2026-04-29, verify east frame quality, render with parallactic angle; no procedural fallback https://claude.ai/code/session_01HkTxpNSTWtViZzxbrbKytR
This commit is contained in:
+34
-70
@@ -85,34 +85,9 @@ OUT_H = int(CONF.get('MOON_OUTPUT_H', CONF.get('MOON_FULL_OUTPUT_H', 1080)))
|
||||
MOON_PCT = float(CONF.get('MOON_HEIGHT_PCT', 0.92))
|
||||
REQUIRE_EAST_VERIFY = CONF.get('MOON_REQUIRE_EAST_VERIFY', 'true').lower() != 'false'
|
||||
|
||||
ATMOSPHERE_BLUR = int(CONF.get('MOON_ATMOSPHERE_BLUR', 0))
|
||||
CLOUDY_POST_ENABLED = CONF.get('MOON_CLOUDY_POST_ENABLED', 'true').lower() != 'false'
|
||||
|
||||
_obs_parts = CONF.get('MOON_OBS_TIME_LOCAL', '22:30').split(':')
|
||||
_obs_parts = CONF.get('MOON_OBS_TIME_LOCAL', '22:00').split(':')
|
||||
OBS_TIME_H = int(_obs_parts[0])
|
||||
OBS_TIME_M = int(_obs_parts[1]) if len(_obs_parts) > 1 else 0
|
||||
OBS_WINDOW_MIN = int(CONF.get('MOON_OBS_WINDOW_MIN', 30))
|
||||
|
||||
|
||||
def _atmosphere_opacity_from_quality(quality: float | None) -> float:
|
||||
"""Map moon detection quality to atmospheric overlay opacity.
|
||||
|
||||
quality >= 0.85 → 0.00 clear sky, no overlay
|
||||
quality 0.70 → 0.20 light haze
|
||||
quality 0.55 → 0.40 noticeable cloud, moon still detected
|
||||
quality < 0.55 → up to 0.65 (overcast fallback frames)
|
||||
quality is None → 0.68 no detection at all, heavy overcast
|
||||
|
||||
The overlay is applied OVER the NASA moon disk, so higher opacity means
|
||||
more of the disk is obscured — physically correct for clouds above us.
|
||||
"""
|
||||
if quality is None:
|
||||
return 0.68
|
||||
if quality >= 0.85:
|
||||
return 0.0
|
||||
# Linear: 0.0 at quality=0.85, 0.40 at quality=0.55
|
||||
raw = (0.85 - quality) / 0.30 * 0.40
|
||||
return min(0.65, raw)
|
||||
|
||||
|
||||
PHASE_SPEC = {
|
||||
@@ -290,73 +265,64 @@ def run_phase(phase: str, target_utc: datetime | None, cam: str,
|
||||
return 2
|
||||
print(f'moon altitude at obs time: {alt_at_obs:.1f}°')
|
||||
|
||||
# Gather east frames in the ±OBS_WINDOW_MIN window
|
||||
obs_start = obs_utc - timedelta(minutes=OBS_WINDOW_MIN)
|
||||
obs_end = obs_utc + timedelta(minutes=OBS_WINDOW_MIN)
|
||||
# Scan east frames in the 22:00–23:00 window for any clear moon detection
|
||||
obs_end = obs_utc + timedelta(hours=1)
|
||||
obs_dates: list[str] = []
|
||||
d = obs_start.astimezone(tz).date()
|
||||
d = obs_utc.astimezone(tz).date()
|
||||
while d <= obs_end.astimezone(tz).date():
|
||||
obs_dates.append(d.strftime('%Y-%m-%d'))
|
||||
d += timedelta(days=1)
|
||||
all_frames = _candidate_frames(cam, obs_dates)
|
||||
window_frames = [(p, dt) for p, dt in all_frames if obs_start <= dt <= obs_end]
|
||||
print(f'east frames in ±{OBS_WINDOW_MIN} min window: {len(window_frames)}')
|
||||
|
||||
# Determine atmosphere opacity from the frame closest to obs_utc
|
||||
east_frame: str | None = None
|
||||
opacity = 0.0
|
||||
window_frames = [(p, dt) for p, dt in all_frames if obs_utc <= dt <= obs_end]
|
||||
print(f'east frames in 22:00-23:00 window: {len(window_frames)}')
|
||||
|
||||
if alt_at_obs < MIN_ALTITUDE:
|
||||
print('moon below horizon at obs time — posting clean NASA image')
|
||||
print(f'moon below horizon at obs time ({alt_at_obs:.1f}deg) — skipping')
|
||||
_notify(
|
||||
f'{spec["emoji"]} {spec["label"]} {target_utc.strftime("%B %Y")} '
|
||||
f'— moon below horizon at {OBS_TIME_H:02d}:{OBS_TIME_M:02d} local',
|
||||
f'Adjust MOON_OBS_TIME_LOCAL in sky-cam.conf. Posting clean NASA render.',
|
||||
f'Adjust MOON_OBS_TIME_LOCAL in sky-cam.conf.',
|
||||
)
|
||||
elif window_frames:
|
||||
best_f = min(window_frames, key=lambda t: abs(t[1] - obs_utc))
|
||||
east_frame, best_dt = best_f
|
||||
det = detect_moon(east_frame)
|
||||
quality = det.quality if det is not None else None
|
||||
opacity = _atmosphere_opacity_from_quality(quality)
|
||||
offset_min = (best_dt - obs_utc).total_seconds() / 60.0
|
||||
print(f'atmosphere frame: {east_frame}')
|
||||
print(f' quality={quality} opacity={opacity:.2f} '
|
||||
f'offset={offset_min:+.1f} min from obs time')
|
||||
else:
|
||||
print('no east frames in obs window — posting clean NASA image')
|
||||
return 0
|
||||
|
||||
# Label atmospheric conditions for the caption
|
||||
if opacity == 0.0:
|
||||
atm_label = 'clear sky'
|
||||
elif opacity < 0.15:
|
||||
atm_label = 'slight haze'
|
||||
elif opacity < 0.40:
|
||||
atm_label = 'cloud cover'
|
||||
else:
|
||||
atm_label = 'heavy overcast'
|
||||
# Binary go/no-go: any frame in the window with quality >= MIN_QUALITY?
|
||||
clear_frame: str | None = None
|
||||
for fpath, fdt in window_frames:
|
||||
det = detect_moon(fpath)
|
||||
if det is not None and det.quality >= MIN_QUALITY:
|
||||
clear_frame = fpath
|
||||
offset_min = (fdt - obs_utc).total_seconds() / 60.0
|
||||
print(f'clear moon detected: {fpath}')
|
||||
print(f' quality={det.quality:.3f} offset=+{offset_min:.1f} min')
|
||||
break
|
||||
else:
|
||||
q = det.quality if det is not None else None
|
||||
print(f' {fpath}: quality={q} — not clear enough')
|
||||
|
||||
if clear_frame is None:
|
||||
print('no clear moon detection in window — skipping (overcast or moon absent)')
|
||||
_notify(
|
||||
f'{spec["emoji"]} {spec["label"]} {target_utc.strftime("%B %Y")} — skipped',
|
||||
f'No clear moon detection in the 22:00-23:00 window.',
|
||||
)
|
||||
return 0
|
||||
|
||||
obs_local_str = _format_local(obs_utc)
|
||||
witness_text = (
|
||||
f'sky-cam {cam} — {atm_label} at {obs_local_str} — NASA SVS Dial-a-Moon'
|
||||
)
|
||||
witness_text = f'sky-cam {cam} — clear at {obs_local_str} — NASA SVS Dial-a-Moon'
|
||||
|
||||
if dry_run:
|
||||
print(f'dry-run: would post with opacity={opacity:.2f} ({atm_label})')
|
||||
print(f'dry-run: would post using NASA image for {obs_utc.isoformat()}')
|
||||
return 0
|
||||
|
||||
return _render_and_post(
|
||||
phase, spec, target_utc, obs_utc, obs_utc.astimezone(tz),
|
||||
cam, dry_run, no_upload, out_path,
|
||||
witness_text=witness_text,
|
||||
east_frame_path=east_frame,
|
||||
atmosphere_opacity=opacity,
|
||||
)
|
||||
|
||||
|
||||
def _render_and_post(phase, spec, target_utc, when_utc, local_dt, cam,
|
||||
dry_run, no_upload, out_path, witness_text,
|
||||
east_frame_path=None, atmosphere_opacity=0.0):
|
||||
dry_run, no_upload, out_path, witness_text):
|
||||
if out_path is None:
|
||||
out_dir = pathlib.Path(MOVIES_DIR) / cam / _output_subdir(phase)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -389,9 +355,7 @@ def _render_and_post(phase, spec, target_utc, when_utc, local_dt, cam,
|
||||
str(nasa_path), out_path,
|
||||
output_size=(OUT_W, OUT_H), moon_height_pct=MOON_PCT,
|
||||
caption=caption,
|
||||
east_frame_path=east_frame_path,
|
||||
atmosphere_opacity=atmosphere_opacity,
|
||||
atmosphere_blur=ATMOSPHERE_BLUR,
|
||||
when_utc=when_utc,
|
||||
)
|
||||
print(f'wrote {out_path}')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user