#!/usr/bin/env python3 """test_moon_composite.py — render a phase composite using real NASA data. Simulates what moon_phase_monthly.py does on the night of a full moon: 1. Round the obs time (22:30 local on 2026-04-29) to the nearest hour. 2. Fetch the NASA SVS Dial-a-Moon render for that UTC hour. 3. Load the east camera frame (21-07-00.jpg, captured at 21:07 UTC that night). 4. Detect atmospheric conditions → compute overlay opacity. 5. Render: NASA moon + east atmosphere overlay → test_composite_out.jpg. Run from the sky-cam directory: python3 test_moon_composite.py Requires internet access to reach svs.gsfc.nasa.gov. If the API is unreachable a procedural moon disc is used as a fallback so the atmospheric overlay is still visible and testable. """ import os import pathlib import sys import tempfile from datetime import datetime, timezone HERE = pathlib.Path(__file__).resolve().parent sys.path.insert(0, str(HERE)) EAST_FRAME = HERE / '21-07-00.jpg' OUT_PATH = HERE / 'test_composite_out.jpg' # The east frame is 2026-04-29 21:07 UTC; obs time is 22:30 local → ~21:30 UTC # (assuming Eastern time UTC-4 in late April). Round to nearest hour → 22:00 UTC. NASA_FETCH_UTC = datetime(2026, 4, 29, 22, 0, tzinfo=timezone.utc) def _fetch_nasa(dt: datetime) -> 'pathlib.Path | None': try: import moon_dialamoon print(f'fetching NASA Dial-a-Moon for {dt.strftime("%Y-%m-%dT%H:00Z")} …') path = moon_dialamoon.fetch_for_time(dt) print(f' cached at {path}') return path except Exception as e: print(f' NASA fetch failed: {e}') return None def _make_procedural_moon(size: int = 2048) -> 'pathlib.Path': """Fallback: clean grey disc with maria and limb darkening.""" from PIL import Image, ImageDraw, ImageFilter import numpy as np img = Image.new('RGB', (size, size), (0, 0, 0)) draw = ImageDraw.Draw(img) cx = cy = size // 2 r = int(size * 0.47) draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=(218, 214, 200)) draw.ellipse([cx - r//3, cy - r//3, cx + r//6, cy + r//5], fill=(170, 167, 154)) draw.ellipse([cx + r//8, cy - r//5, cx + r//3, cy + r//8], fill=(182, 179, 166)) draw.ellipse([cx - r//4, cy + r//6, cx + r//8, cy + r//3], fill=(175, 172, 159)) draw.ellipse([cx - r//2, cy - r//10, cx - r//5, cy + r//4], fill=(185, 182, 169)) img = img.filter(ImageFilter.GaussianBlur(radius=size // 80)) vignette = Image.new('L', (size, size), 0) vd = ImageDraw.Draw(vignette) vd.ellipse([cx - r, cy - r, cx + r, cy + r], fill=255) vignette = vignette.filter(ImageFilter.GaussianBlur(radius=size // 30)) arr = np.asarray(img).astype(float) vig = np.asarray(vignette).astype(float) / 255.0 arr = np.clip(arr * (0.75 + 0.25 * vig)[..., None], 0, 255).astype('uint8') img = Image.fromarray(arr) tmp = tempfile.NamedTemporaryFile(suffix='.png', delete=False) tmp.close() img.save(tmp.name) return pathlib.Path(tmp.name) def _detect_atmosphere(frame_path: str) -> tuple[float | None, float]: """Run moon_detect and return (quality, opacity).""" try: from moon_detect import detect_moon from moon_phase_monthly import _atmosphere_opacity_from_quality det = detect_moon(frame_path) quality = det.quality if det is not None else None opacity = _atmosphere_opacity_from_quality(quality) return quality, opacity except Exception as e: print(f' detection failed: {e}') return None, 0.0 def main(): if not EAST_FRAME.exists(): print(f'ERROR: missing {EAST_FRAME}', file=sys.stderr) sys.exit(1) print('=== moon composite test ===') print(f'east frame : {EAST_FRAME.name} (2026-04-29 21:07 UTC)') print(f'NASA time : {NASA_FETCH_UTC.strftime("%Y-%m-%dT%H:00Z")}') print() # 1. Try real NASA image tmp_to_delete = None nasa_path = _fetch_nasa(NASA_FETCH_UTC) if nasa_path is None: print('falling back to procedural moon disc …') nasa_path = _make_procedural_moon() tmp_to_delete = str(nasa_path) print(f' disc saved to {nasa_path}') print() # 2. Atmosphere from east frame print(f'checking atmosphere in {EAST_FRAME.name} …') quality, opacity = _detect_atmosphere(str(EAST_FRAME)) if quality is not None: print(f' moon quality: {quality:.3f} → atmosphere opacity: {opacity:.2f}') else: print(f' no moon detected (overcast) → opacity: {opacity:.2f}') print() # 3. Render from moon_composite import render_phase_closeup caption = ( f'Full Moon — April 2026 — ' f'sky-cam east 2026-04-29 22:30 local — ' f'NASA SVS Dial-a-Moon' ) print(f'rendering {OUT_PATH.name} …') try: render_phase_closeup( nasa_render_path=str(nasa_path), out_path=str(OUT_PATH), output_size=(1920, 1080), moon_height_pct=0.88, caption=caption, east_frame_path=str(EAST_FRAME), atmosphere_opacity=opacity, atmosphere_blur=0, ) finally: if tmp_to_delete: os.unlink(tmp_to_delete) print(f'done → {OUT_PATH}') print() print(f'opacity={opacity:.2f}:', end=' ') if opacity == 0.0: print('clear sky — pure NASA moon on black background') elif opacity < 0.15: print('slight haze — moon visible, softly veiled') elif opacity < 0.40: print('cloud cover — moon partially obscured') else: print('heavy overcast — moon a glow through cloud') if __name__ == '__main__': main()