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:
Claude
2026-05-01 22:27:34 +00:00
parent 9e2852fd73
commit a6989a9ea8
4 changed files with 98 additions and 268 deletions
+11 -46
View File
@@ -240,29 +240,6 @@ def composite_full_moon(
return out_path
def _make_atmosphere_layer(
east_frame_path: str,
output_size: tuple[int, int],
blur_radius: int = 0,
) -> Image.Image:
"""Scale the full east frame to output_size and blur to atmospheric haze.
The blur removes wide-angle camera detail (RTSP artefacts, OSD text,
pixel noise) while preserving real sky colour and large-scale cloud
structure. The resulting layer is applied OVER the NASA moon disk so
it reads as "clouds between the observer and the moon" — which is
physically correct.
blur_radius=0 → auto: output_width // 20 (96 px at 1920 wide).
This smooths pixel-level RTSP noise and OSD text while keeping
recognisable cloud shapes and sky-colour gradients intact.
"""
src = Image.open(east_frame_path).convert('RGB')
layer = src.resize(output_size, LANCZOS)
r = blur_radius if blur_radius > 0 else output_size[0] // 20
return layer.filter(ImageFilter.GaussianBlur(radius=r))
def render_phase_closeup(
nasa_render_path: str,
out_path: str,
@@ -270,28 +247,16 @@ def render_phase_closeup(
moon_height_pct: float = 0.92,
caption: str | None = None,
background: tuple[int, int, int] = (0, 0, 0),
east_frame_path: str | None = None,
atmosphere_opacity: float = 0.0,
atmosphere_blur: int = 0,
when_utc: datetime | None = None,
):
"""Full-screen close-up rendering using a NASA SVS Dial-a-Moon image.
Rendering pipeline:
1. Black background (outer space).
2. NASA moon disk — correct phase, libration, crater shadows — centred
and scaled to moon_height_pct of frame height.
3. Atmospheric layer (optional): east's camera frame, scaled to output
size and blurred, composited OVER the moon at atmosphere_opacity.
This is physically correct — clouds are between the observer and the
moon so they occlude the disk, not sit behind it.
atmosphere_opacity controls what the viewer sees through east's sky:
0.00 — perfectly clear: pure NASA render, no overlay
0.10 — slight haze: moon is sharp but slightly softened
0.35 — noticeable cloud cover: moon partially obscured
0.65 — heavy overcast: moon a faint glow through thick cloud
Renders the NASA moon disk centred on a black background, rotated by
the parallactic angle so its orientation matches what east's camera sees
from its geographic location at the given UTC time.
"""
# ── Background + NASA moon ────────────────────────────────────────────
import moon_phase
bg = Image.new('RGB', output_size, background)
moon = Image.open(nasa_render_path).convert('RGB')
moon = _square_crop_to_disk(moon)
@@ -300,6 +265,11 @@ def render_phase_closeup(
target += target % 2
moon_resized = moon.resize((target, target), LANCZOS)
# Rotate by parallactic angle so "up" on the moon matches east's sky
if when_utc is not None:
par = moon_phase.parallactic_angle(when_utc)
moon_resized = moon_resized.rotate(-par, resample=BICUBIC, expand=False)
feather = max(3, target // 240)
mask = _disk_mask(target, feather_px=feather)
@@ -307,11 +277,6 @@ def render_phase_closeup(
py = (output_size[1] - target) // 2
bg.paste(moon_resized, (px, py), mask)
# ── Atmospheric layer from east frame — applied OVER the moon ─────────
if east_frame_path and atmosphere_opacity > 0.0:
atm = _make_atmosphere_layer(east_frame_path, output_size, atmosphere_blur)
bg = Image.blend(bg, atm, alpha=atmosphere_opacity)
if caption:
draw = ImageDraw.Draw(bg)
_draw_caption(draw, caption, (22, output_size[1] - 48), output_size[0])