Files
sky-cam/test_moon_composite.py
T
Claude 1425a6e5f4 Replace annular cloud veil with full-frame east sky backdrop
The annular-ring veil approach sampled too small a region (38 px moon
in 3840×2160 gives a tiny annulus) and was invisible in practice.
More fundamentally, using the whole east sky is what "relayed to where
it was taken" actually means.

New approach — _make_east_sky_backdrop():
  Scale the full east camera frame to output size, apply GaussianBlur
  r = output_width // 6 (≈ 320 px at 1920 wide).  The blur erases RTSP
  artefacts, OSD overlays, and the wide-angle look while preserving real
  sky colour, cloud / haze gradients, and the dark ground silhouette.
  The NASA moon disk is pasted sharp on top.  The result reads as east
  photographed the moon through a telephoto with its actual sky that night.

  Clear dark sky → nearly black backdrop (same feel as before).
  Thin cloud cover → soft grey-blue haze behind the sharp moon.
  Heavy overcast → the moon detection would not qualify, so this case
    never reaches rendering.

Config rename: MOON_CLOUD_OVERLAY_* → MOON_EAST_SKY_ENABLED / _BLUR.
moon_phase_monthly.py: CLOUD_OVERLAY_* → EAST_SKY_*.
render_phase_closeup(): cloud_overlay_* params → east_sky_*.

test_moon_composite.py: replace full_moon_closeup.jpg (timestamped
photograph) with a procedural grey disc generated via PIL + numpy so the
test does not look like it is reusing an existing image.

https://claude.ai/code/session_01HkTxpNSTWtViZzxbrbKytR
2026-05-01 18:17:54 +00:00

128 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""test_moon_composite.py — smoke-test the east-sky-backdrop composite.
Uses the sample east frame (21-07-00.jpg) already in the repo and a
procedurally generated moon disc (no camera timestamp, no copyright) to
demonstrate the east-sky backdrop without needing a live NASA API call.
Run from the sky-cam directory:
python3 test_moon_composite.py
Output: test_composite_out.jpg
The east frame (2026-04-29 21:06:45) shows the moon through visible thin
cloud cover; that sky — blurred to atmospheric haze — becomes the backdrop
behind the procedural moon disc. In production the disc is replaced by the
NASA SVS Dial-a-Moon render for the exact UTC hour east captured the moon.
"""
import pathlib
import sys
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'
def _make_procedural_moon(size: int = 2048) -> 'Image':
"""Generate a clean grey disc that stands in for a NASA Dial-a-Moon render.
Draws a base disc, a few darker ellipses for lunar maria, and a subtle
limb-darkening gradient. No timestamp, no copyright, no camera artefacts.
"""
from PIL import Image, ImageDraw, ImageFilter
img = Image.new('RGB', (size, size), (0, 0, 0))
draw = ImageDraw.Draw(img)
cx = cy = size // 2
r = int(size * 0.47)
# Base disc — warm grey, slightly off-white like a real moon
draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=(218, 214, 200))
# A few darker patches suggesting the major maria
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))
# Smooth the hard edges so it blends naturally
img = img.filter(ImageFilter.GaussianBlur(radius=size // 80))
# Subtle limb darkening: blend a radial dark vignette at the disc edge
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))
import numpy as np
arr = np.asarray(img).astype(float)
vig = np.asarray(vignette).astype(float) / 255.0
# Darken towards the limb (where vig is small → near edge)
darken = 0.75 + 0.25 * vig # 0.75 at edge, 1.0 at centre
arr = np.clip(arr * darken[..., None], 0, 255).astype('uint8')
img = Image.fromarray(arr)
return img
def main():
if not EAST_FRAME.exists():
print(f'ERROR: missing {EAST_FRAME}', file=sys.stderr)
sys.exit(1)
# Save the procedural disc to a temp file so render_phase_closeup can open it
import tempfile, os
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tf:
tmp_moon = tf.name
try:
print('=== moon composite smoke-test ===')
print(f'east frame : {EAST_FRAME.name}')
print(f'moon disc : procedural (NASA Dial-a-Moon stand-in)')
print(f'output : {OUT_PATH.name}')
print()
print('generating procedural moon disc …')
moon_img = _make_procedural_moon(2048)
moon_img.save(tmp_moon)
from moon_composite import render_phase_closeup
caption = (
'Full Moon — April 2026 — '
'sky-cam east witnessed at 2026-04-29 21:07:00 UTC '
'(+7 min from exact full) — render: NASA SVS Dial-a-Moon'
)
print('rendering composite with east sky backdrop …')
render_phase_closeup(
nasa_render_path=tmp_moon,
out_path=str(OUT_PATH),
output_size=(1920, 1080),
moon_height_pct=0.88,
caption=caption,
east_frame_path=str(EAST_FRAME),
east_sky_enabled=True,
east_sky_blur=0, # auto: output_width / 6 = 320 px
)
finally:
os.unlink(tmp_moon)
print(f'\ndone → {OUT_PATH}')
print()
print('What you should see:')
print(' • Background: easts April 29 night sky, blurred to soft atmospheric haze')
print(' (thin cloud cover visible as a grey-blue gradient, treeline at bottom)')
print(' • Moon disc filling ~88% of the 1920×1080 frame, pasted sharp on top')
print(' • Caption at bottom-left with phase / date / witness text')
print()
print('In production the procedural disc is replaced by the NASA SVS Dial-a-Moon')
print('render for that exact UTC hour — same layout, real crater detail.')
if __name__ == '__main__':
main()