Files
sky-cam/test_moon_composite.py
T
Claude c09b872f52 Apply atmosphere overlay ON TOP of moon, add overcast fallback
The core mental model was wrong: clouds are between the observer and
the moon, so they occlude the disk — they don't sit behind it.

moon_composite.py:
  _make_east_sky_backdrop() → _make_atmosphere_layer()
  render_phase_closeup() pipeline is now:
    1. Black background
    2. NASA moon disk centred (correct phase / libration / shadows)
    3. East frame scaled + blurred → composited OVER the disk at
       atmosphere_opacity (0.0 = clear, 0.68 = heavy overcast)
  Parameters: east_sky_enabled/blur → atmosphere_opacity/blur

moon_phase_monthly.py:
  _atmosphere_opacity_from_quality() maps detection quality to opacity:
    quality >= 0.85 → 0.00 (clear)
    quality   0.70  → 0.20 (light haze)
    quality   0.55  → 0.40 (notable cloud, still detected)
    quality   None  → 0.68 (no detection, heavy overcast)

  Frame scan now tracks two lists:
    qualifying     — frames passing quality + illumination (existing)
    above_horizon  — frames where moon is up but detection failed

  When qualifying is empty and MOON_CLOUDY_POST_ENABLED=true, the
  above-horizon frame closest to the exact phase moment is used with
  opacity 0.45–0.68.  The month is always represented — full moon,
  first quarter, and third quarter each get a post even in cloudy
  months, showing the moon as a faint glow behind cloud.

sky-cam.conf:
  MOON_EAST_SKY_ENABLED/BLUR → MOON_ATMOSPHERE_BLUR (opacity is
  computed automatically from quality, not configured directly)
  New: MOON_CLOUDY_POST_ENABLED=true

test_moon_composite.py:
  Generates three outputs at opacity 0.00 / 0.20 / 0.65 so the
  full opacity range is visible in one test run.

https://claude.ai/code/session_01HkTxpNSTWtViZzxbrbKytR
2026-05-01 21:51:42 +00:00

116 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""test_moon_composite.py — smoke-test atmospheric overlay at three opacity levels.
Generates three output images from the same east camera frame (21-07-00.jpg)
to show how the atmospheric overlay looks across the quality spectrum:
test_composite_clear.jpg — opacity 0.00 (quality ≥ 0.85, clear sky)
test_composite_hazy.jpg — opacity 0.20 (quality ~ 0.70, light haze)
test_composite_cloudy.jpg — opacity 0.65 (overcast fallback)
The moon disk in each image is procedurally generated (clean grey sphere,
no camera timestamp). In production it is replaced by the NASA SVS
Dial-a-Moon render for the exact UTC hour east captured the moon.
Run from the sky-cam directory:
python3 test_moon_composite.py
"""
import os
import pathlib
import sys
import tempfile
HERE = pathlib.Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
EAST_FRAME = HERE / '21-07-00.jpg'
CASES = [
('test_composite_clear.jpg', 0.00, 'clear sky (quality >= 0.85)'),
('test_composite_hazy.jpg', 0.20, 'light haze (quality ~ 0.70)'),
('test_composite_cloudy.jpg', 0.65, 'heavy overcast fallback'),
]
def _make_procedural_moon(size: int = 2048) -> 'Image':
"""Clean grey disc with simplified lunar 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')
return Image.fromarray(arr)
def main():
if not EAST_FRAME.exists():
print(f'ERROR: missing {EAST_FRAME}', file=sys.stderr)
sys.exit(1)
print('generating procedural moon disc …')
moon_img = _make_procedural_moon(2048)
tmp = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
tmp_moon = tmp.name
tmp.close()
moon_img.save(tmp_moon)
from moon_composite import render_phase_closeup
try:
for filename, opacity, label in CASES:
out = HERE / filename
caption = (
f'Full Moon — April 2026 — '
f'sky-cam east 2026-04-29 21:07 UTC — '
f'NASA SVS Dial-a-Moon [{label}]'
)
print(f'rendering {filename} (opacity={opacity:.2f}) {label} …')
render_phase_closeup(
nasa_render_path=tmp_moon,
out_path=str(out),
output_size=(1920, 1080),
moon_height_pct=0.88,
caption=caption,
east_frame_path=str(EAST_FRAME),
atmosphere_opacity=opacity,
atmosphere_blur=0,
)
print(f' -> {out}')
finally:
os.unlink(tmp_moon)
print()
print('done. Three outputs:')
for filename, opacity, label in CASES:
print(f' {filename:35s} opacity={opacity:.2f} {label}')
print()
print('What you should see in each:')
print(' clear — NASA moon disk sharp and unobscured on black background')
print(' hazy — same moon but with a soft grey-blue veil over the disk')
print(' (from the thin cloud visible in 21-07-00.jpg)')
print(' cloudy — moon mostly hidden; visible as a bright glow through')
print(' the cloud texture from east\'s April 29 frame')
if __name__ == '__main__':
main()