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
This commit is contained in:
+34
-34
@@ -240,27 +240,26 @@ def composite_full_moon(
|
||||
return out_path
|
||||
|
||||
|
||||
def _make_east_sky_backdrop(
|
||||
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 it heavily.
|
||||
"""Scale the full east frame to output_size and blur to atmospheric haze.
|
||||
|
||||
The blur removes wide-angle camera detail (pixel noise, RTSP compression
|
||||
artefacts, OSD text) while preserving the real atmospheric colours, any
|
||||
cloud patterns, and the dark ground silhouette at the bottom of frame.
|
||||
The result reads as "this is the sky east saw that night" rather than a
|
||||
stretched wide-angle photo.
|
||||
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 chooses automatically: output_width // 6, which gives a
|
||||
soft impressionistic backdrop while still letting cloud shapes show
|
||||
through as gentle colour gradients.
|
||||
blur_radius=0 → auto: output_width // 10, which smooths pixel-level
|
||||
detail but keeps cloud-scale gradients visible.
|
||||
"""
|
||||
src = Image.open(east_frame_path).convert('RGB')
|
||||
backdrop = src.resize(output_size, LANCZOS)
|
||||
r = blur_radius if blur_radius > 0 else output_size[0] // 6
|
||||
return backdrop.filter(ImageFilter.GaussianBlur(radius=r))
|
||||
layer = src.resize(output_size, LANCZOS)
|
||||
r = blur_radius if blur_radius > 0 else output_size[0] // 10
|
||||
return layer.filter(ImageFilter.GaussianBlur(radius=r))
|
||||
|
||||
|
||||
def render_phase_closeup(
|
||||
@@ -271,32 +270,28 @@ def render_phase_closeup(
|
||||
caption: str | None = None,
|
||||
background: tuple[int, int, int] = (0, 0, 0),
|
||||
east_frame_path: str | None = None,
|
||||
east_sky_enabled: bool = True,
|
||||
east_sky_blur: int = 0,
|
||||
atmosphere_opacity: float = 0.0,
|
||||
atmosphere_blur: int = 0,
|
||||
):
|
||||
"""Full-screen close-up rendering using a NASA SVS Dial-a-Moon image.
|
||||
|
||||
The dial-a-moon render has the correct phase, libration and crater shadows
|
||||
for the requested timestamp. We size it to fill the output frame and add
|
||||
a caption.
|
||||
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.
|
||||
|
||||
When east_frame_path is provided the east camera's frame for that night is
|
||||
scaled to output_size and blurred heavily (GaussianBlur r ≈ output_width/6)
|
||||
to produce an atmospheric backdrop — east's real night sky colour, any
|
||||
cloud or haze patterns, and the dark ground silhouette at the bottom of
|
||||
frame all show through the blur as soft gradients. The NASA moon disk is
|
||||
then pasted sharp on top of that backdrop.
|
||||
|
||||
This is what "relayed to where it was taken" looks like: the sky behind
|
||||
the NASA moon is east's actual sky from that hour. Set
|
||||
east_sky_enabled=False (or leave east_frame_path=None) to use a plain
|
||||
black background instead.
|
||||
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
|
||||
"""
|
||||
if east_sky_enabled and east_frame_path:
|
||||
bg = _make_east_sky_backdrop(east_frame_path, output_size, east_sky_blur)
|
||||
else:
|
||||
bg = Image.new('RGB', output_size, background)
|
||||
|
||||
# ── Background + NASA moon ────────────────────────────────────────────
|
||||
bg = Image.new('RGB', output_size, background)
|
||||
moon = Image.open(nasa_render_path).convert('RGB')
|
||||
moon = _square_crop_to_disk(moon)
|
||||
|
||||
@@ -311,6 +306,11 @@ 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])
|
||||
|
||||
+61
-20
@@ -85,8 +85,29 @@ 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'
|
||||
|
||||
EAST_SKY_ENABLED = CONF.get('MOON_EAST_SKY_ENABLED', 'true').lower() != 'false'
|
||||
EAST_SKY_BLUR = int(CONF.get('MOON_EAST_SKY_BLUR', 0))
|
||||
ATMOSPHERE_BLUR = int(CONF.get('MOON_ATMOSPHERE_BLUR', 0))
|
||||
CLOUDY_POST_ENABLED = CONF.get('MOON_CLOUDY_POST_ENABLED', 'true').lower() != 'false'
|
||||
|
||||
|
||||
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 = {
|
||||
@@ -277,6 +298,7 @@ def run_phase(phase: str, target_utc: datetime | None, cam: str,
|
||||
return 0
|
||||
|
||||
qualifying = []
|
||||
above_horizon = [] # frames where moon is up but quality/illum check failed
|
||||
for path, utc_dt in candidates:
|
||||
try:
|
||||
alt, _ = moon_phase.altaz(utc_dt)
|
||||
@@ -287,6 +309,7 @@ def run_phase(phase: str, target_utc: datetime | None, cam: str,
|
||||
continue
|
||||
det = detect_moon(path)
|
||||
if det is None or det.quality < MIN_QUALITY:
|
||||
above_horizon.append((path, utc_dt, det))
|
||||
continue
|
||||
illum = moon_phase.illumination(utc_dt)
|
||||
if illum < spec['illum_min'] or illum > spec['illum_max']:
|
||||
@@ -296,21 +319,38 @@ def run_phase(phase: str, target_utc: datetime | None, cam: str,
|
||||
continue
|
||||
qualifying.append((path, utc_dt, det, alt, illum))
|
||||
|
||||
print(f'qualifying frames: {len(qualifying)}')
|
||||
print(f'qualifying frames: {len(qualifying)} above-horizon fallback pool: {len(above_horizon)}')
|
||||
|
||||
if not qualifying:
|
||||
# No frame met quality + illumination thresholds — likely overcast.
|
||||
# If MOON_CLOUDY_POST_ENABLED, use the above-horizon frame closest to
|
||||
# the exact phase moment as the atmosphere source and post with a
|
||||
# heavy cloud overlay so the month is still represented.
|
||||
if above_horizon and CLOUDY_POST_ENABLED:
|
||||
best_cloudy = min(above_horizon, key=lambda t: abs(t[1] - target_utc))
|
||||
path, _when, det = best_cloudy
|
||||
quality = det.quality if det is not None else None
|
||||
opacity = _atmosphere_opacity_from_quality(quality)
|
||||
print(f'overcast fallback: {path} quality={quality} opacity={opacity:.2f}')
|
||||
witness = f'cloud cover — {target_utc.strftime("%Y-%m-%d")} — NASA SVS Dial-a-Moon'
|
||||
if dry_run:
|
||||
return 0
|
||||
return _render_and_post(
|
||||
phase, spec, target_utc, target_utc,
|
||||
target_utc.astimezone(tz),
|
||||
cam, dry_run, no_upload, out_path,
|
||||
witness_text=witness,
|
||||
east_frame_path=path,
|
||||
atmosphere_opacity=opacity,
|
||||
)
|
||||
|
||||
msg = (
|
||||
f'No clear-shot {phase} frame in window {dates[0]}..{dates[-1]} '
|
||||
f'(need quality≥{MIN_QUALITY}, altitude≥{MIN_ALTITUDE}°, '
|
||||
f'illumination {spec["illum_min"]:.2f}-{spec["illum_max"]:.2f}). '
|
||||
f'No {phase} frame in window {dates[0]}..{dates[-1]} '
|
||||
f'(quality≥{MIN_QUALITY}, altitude≥{MIN_ALTITUDE}°, '
|
||||
f'illumination {spec["illum_min"]:.2f}-{spec["illum_max"]:.2f}) '
|
||||
f'and no above-horizon frames for cloudy fallback.'
|
||||
)
|
||||
if phase == 'first-quarter':
|
||||
msg += ('First quarter from east is best-effort because the moon is '
|
||||
'only up during daylight hours — daytime detection often '
|
||||
'fails. Lower MOON_MIN_QUALITY or accept that some months '
|
||||
'will skip.')
|
||||
else:
|
||||
msg += 'Likely cloudy across the whole window.'
|
||||
_notify(f'{spec["emoji"]} {spec["label"]} — no clear shot {target_utc.strftime("%B %Y")}', msg)
|
||||
_notify(f'{spec["emoji"]} {spec["label"]} — skipped {target_utc.strftime("%B %Y")}', msg)
|
||||
print(msg)
|
||||
return 0
|
||||
|
||||
@@ -318,10 +358,11 @@ def run_phase(phase: str, target_utc: datetime | None, cam: str,
|
||||
path, when_utc, det, alt, illum = best
|
||||
local_dt = when_utc.astimezone(tz)
|
||||
delta_min = (when_utc - target_utc).total_seconds() / 60.0
|
||||
opacity = _atmosphere_opacity_from_quality(det.quality)
|
||||
print(f'picked: {path}')
|
||||
print(f' when_utc={when_utc.isoformat()} local={local_dt} '
|
||||
f'altitude={alt:.1f}° illum={illum:.4f} quality={det.quality:.3f} '
|
||||
f'Δtarget={delta_min:+.1f} min')
|
||||
f'altitude={alt:.1f} illum={illum:.4f} quality={det.quality:.3f} '
|
||||
f'opacity={opacity:.2f} delta={delta_min:+.1f} min')
|
||||
|
||||
if dry_run:
|
||||
return 0
|
||||
@@ -332,13 +373,13 @@ def run_phase(phase: str, target_utc: datetime | None, cam: str,
|
||||
witness_text=f'witnessed at {local_dt.strftime("%Y-%m-%d %H:%M:%S %Z")} '
|
||||
f'({delta_min:+.0f} min from exact {phase})',
|
||||
east_frame_path=path,
|
||||
east_detection=det,
|
||||
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, east_detection=None):
|
||||
east_frame_path=None, atmosphere_opacity=0.0):
|
||||
if out_path is None:
|
||||
out_dir = pathlib.Path(MOVIES_DIR) / cam / _output_subdir(phase)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -372,8 +413,8 @@ def _render_and_post(phase, spec, target_utc, when_utc, local_dt, cam,
|
||||
output_size=(OUT_W, OUT_H), moon_height_pct=MOON_PCT,
|
||||
caption=caption,
|
||||
east_frame_path=east_frame_path,
|
||||
east_sky_enabled=EAST_SKY_ENABLED,
|
||||
east_sky_blur=EAST_SKY_BLUR,
|
||||
atmosphere_opacity=atmosphere_opacity,
|
||||
atmosphere_blur=ATMOSPHERE_BLUR,
|
||||
)
|
||||
print(f'wrote {out_path}')
|
||||
|
||||
|
||||
+23
-15
@@ -524,24 +524,32 @@ MOON_HEIGHT_PCT=0.92 # moon disk fills this fraction of frame heig
|
||||
MOON_DIALAMOON_TARGET_PX=2048 # cached PNG longest side; downsampled on save
|
||||
MOON_DIALAMOON_TIMEOUT_SEC=30
|
||||
|
||||
# East sky backdrop ───────────────────────────────────────────────────────────
|
||||
# When east verifies a moon sighting, its full camera frame is scaled to
|
||||
# output size and blurred heavily (GaussianBlur r ≈ output_width / 6) to
|
||||
# produce an atmospheric backdrop behind the NASA moon disk.
|
||||
# Atmospheric overlay ─────────────────────────────────────────────────────────
|
||||
# East's camera frame is scaled to output size, blurred, and composited OVER
|
||||
# the NASA moon disk. This is physically correct: clouds sit between the
|
||||
# observer and the moon, so they occlude the disk rather than appear behind it.
|
||||
#
|
||||
# The blur removes RTSP artefacts, OSD text, and the wide-angle look while
|
||||
# preserving the real sky colour, any cloud or haze gradients, and the dark
|
||||
# tree/ground silhouette at the bottom of frame. The NASA moon is pasted
|
||||
# sharp on top — the result looks like east shot it through a telephoto, with
|
||||
# its actual sky that night as the background.
|
||||
# Opacity is derived from the moon detection quality in that frame:
|
||||
# quality ≥ 0.85 → 0% (clear sky — pure NASA render)
|
||||
# quality 0.70 → 20% (light haze)
|
||||
# quality 0.55 → 40% (notable cloud, moon still detected)
|
||||
# quality < 0.55 → 40–65% (overcast fallback — see MOON_CLOUDY_POST_ENABLED)
|
||||
# no detection → 68% (heavy overcast)
|
||||
#
|
||||
# This is the honest solution to "east doesn't capture high-res clouds": east's
|
||||
# real atmospheric fingerprint (dark and clear, softly hazy, or cloud-diffused)
|
||||
# becomes the background without pretending to photograph detail that isn't there.
|
||||
# blur radius: 0 = auto (output_width / 10); set in px to override.
|
||||
#MOON_ATMOSPHERE_BLUR=0
|
||||
#
|
||||
# Set false to use a plain black background instead (original behaviour).
|
||||
MOON_EAST_SKY_ENABLED=true
|
||||
#MOON_EAST_SKY_BLUR=0 # blur radius in px; 0 = auto (output_width / 6)
|
||||
# Cloudy-month fallback ───────────────────────────────────────────────────────
|
||||
# If no frame in the collection window passes the quality + illumination
|
||||
# thresholds, the script normally skips posting that month. With
|
||||
# MOON_CLOUDY_POST_ENABLED=true it instead finds the above-horizon frame
|
||||
# closest to the exact phase moment, applies a heavy atmospheric overlay
|
||||
# (opacity 0.45–0.68), and posts the month anyway — the moon shows as a faint
|
||||
# glow behind cloud rather than being absent entirely.
|
||||
#
|
||||
# This applies to all three phases: full, first-quarter, third-quarter.
|
||||
# Caption will read "cloud cover — YYYY-MM-DD — NASA SVS Dial-a-Moon".
|
||||
MOON_CLOUDY_POST_ENABLED=true
|
||||
|
||||
# ── Mattermost — daily sunrise upload ─────────────────────────────────────────
|
||||
# mattermost_url, access_token, channel_id go in .env (see bottom of this file).
|
||||
|
||||
+62
-74
@@ -1,72 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""test_moon_composite.py — smoke-test the east-sky-backdrop composite.
|
||||
"""test_moon_composite.py — smoke-test atmospheric overlay at three opacity levels.
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
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 os
|
||||
import pathlib
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
import tempfile
|
||||
|
||||
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'
|
||||
|
||||
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':
|
||||
"""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.
|
||||
"""
|
||||
"""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)
|
||||
|
||||
# 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
|
||||
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():
|
||||
@@ -74,53 +66,49 @@ def main():
|
||||
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
|
||||
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:
|
||||
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
|
||||
)
|
||||
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(f'\ndone → {OUT_PATH}')
|
||||
print()
|
||||
print('What you should see:')
|
||||
print(' • Background: east’s 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('done. Three outputs:')
|
||||
for filename, opacity, label in CASES:
|
||||
print(f' {filename:35s} opacity={opacity:.2f} {label}')
|
||||
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.')
|
||||
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__':
|
||||
|
||||
Reference in New Issue
Block a user