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:
+11
-46
@@ -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])
|
||||
|
||||
+34
-70
@@ -85,34 +85,9 @@ 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'
|
||||
|
||||
ATMOSPHERE_BLUR = int(CONF.get('MOON_ATMOSPHERE_BLUR', 0))
|
||||
CLOUDY_POST_ENABLED = CONF.get('MOON_CLOUDY_POST_ENABLED', 'true').lower() != 'false'
|
||||
|
||||
_obs_parts = CONF.get('MOON_OBS_TIME_LOCAL', '22:30').split(':')
|
||||
_obs_parts = CONF.get('MOON_OBS_TIME_LOCAL', '22:00').split(':')
|
||||
OBS_TIME_H = int(_obs_parts[0])
|
||||
OBS_TIME_M = int(_obs_parts[1]) if len(_obs_parts) > 1 else 0
|
||||
OBS_WINDOW_MIN = int(CONF.get('MOON_OBS_WINDOW_MIN', 30))
|
||||
|
||||
|
||||
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 = {
|
||||
@@ -290,73 +265,64 @@ def run_phase(phase: str, target_utc: datetime | None, cam: str,
|
||||
return 2
|
||||
print(f'moon altitude at obs time: {alt_at_obs:.1f}°')
|
||||
|
||||
# Gather east frames in the ±OBS_WINDOW_MIN window
|
||||
obs_start = obs_utc - timedelta(minutes=OBS_WINDOW_MIN)
|
||||
obs_end = obs_utc + timedelta(minutes=OBS_WINDOW_MIN)
|
||||
# Scan east frames in the 22:00–23:00 window for any clear moon detection
|
||||
obs_end = obs_utc + timedelta(hours=1)
|
||||
obs_dates: list[str] = []
|
||||
d = obs_start.astimezone(tz).date()
|
||||
d = obs_utc.astimezone(tz).date()
|
||||
while d <= obs_end.astimezone(tz).date():
|
||||
obs_dates.append(d.strftime('%Y-%m-%d'))
|
||||
d += timedelta(days=1)
|
||||
all_frames = _candidate_frames(cam, obs_dates)
|
||||
window_frames = [(p, dt) for p, dt in all_frames if obs_start <= dt <= obs_end]
|
||||
print(f'east frames in ±{OBS_WINDOW_MIN} min window: {len(window_frames)}')
|
||||
|
||||
# Determine atmosphere opacity from the frame closest to obs_utc
|
||||
east_frame: str | None = None
|
||||
opacity = 0.0
|
||||
window_frames = [(p, dt) for p, dt in all_frames if obs_utc <= dt <= obs_end]
|
||||
print(f'east frames in 22:00-23:00 window: {len(window_frames)}')
|
||||
|
||||
if alt_at_obs < MIN_ALTITUDE:
|
||||
print('moon below horizon at obs time — posting clean NASA image')
|
||||
print(f'moon below horizon at obs time ({alt_at_obs:.1f}deg) — skipping')
|
||||
_notify(
|
||||
f'{spec["emoji"]} {spec["label"]} {target_utc.strftime("%B %Y")} '
|
||||
f'— moon below horizon at {OBS_TIME_H:02d}:{OBS_TIME_M:02d} local',
|
||||
f'Adjust MOON_OBS_TIME_LOCAL in sky-cam.conf. Posting clean NASA render.',
|
||||
f'Adjust MOON_OBS_TIME_LOCAL in sky-cam.conf.',
|
||||
)
|
||||
elif window_frames:
|
||||
best_f = min(window_frames, key=lambda t: abs(t[1] - obs_utc))
|
||||
east_frame, best_dt = best_f
|
||||
det = detect_moon(east_frame)
|
||||
quality = det.quality if det is not None else None
|
||||
opacity = _atmosphere_opacity_from_quality(quality)
|
||||
offset_min = (best_dt - obs_utc).total_seconds() / 60.0
|
||||
print(f'atmosphere frame: {east_frame}')
|
||||
print(f' quality={quality} opacity={opacity:.2f} '
|
||||
f'offset={offset_min:+.1f} min from obs time')
|
||||
else:
|
||||
print('no east frames in obs window — posting clean NASA image')
|
||||
return 0
|
||||
|
||||
# Label atmospheric conditions for the caption
|
||||
if opacity == 0.0:
|
||||
atm_label = 'clear sky'
|
||||
elif opacity < 0.15:
|
||||
atm_label = 'slight haze'
|
||||
elif opacity < 0.40:
|
||||
atm_label = 'cloud cover'
|
||||
else:
|
||||
atm_label = 'heavy overcast'
|
||||
# Binary go/no-go: any frame in the window with quality >= MIN_QUALITY?
|
||||
clear_frame: str | None = None
|
||||
for fpath, fdt in window_frames:
|
||||
det = detect_moon(fpath)
|
||||
if det is not None and det.quality >= MIN_QUALITY:
|
||||
clear_frame = fpath
|
||||
offset_min = (fdt - obs_utc).total_seconds() / 60.0
|
||||
print(f'clear moon detected: {fpath}')
|
||||
print(f' quality={det.quality:.3f} offset=+{offset_min:.1f} min')
|
||||
break
|
||||
else:
|
||||
q = det.quality if det is not None else None
|
||||
print(f' {fpath}: quality={q} — not clear enough')
|
||||
|
||||
if clear_frame is None:
|
||||
print('no clear moon detection in window — skipping (overcast or moon absent)')
|
||||
_notify(
|
||||
f'{spec["emoji"]} {spec["label"]} {target_utc.strftime("%B %Y")} — skipped',
|
||||
f'No clear moon detection in the 22:00-23:00 window.',
|
||||
)
|
||||
return 0
|
||||
|
||||
obs_local_str = _format_local(obs_utc)
|
||||
witness_text = (
|
||||
f'sky-cam {cam} — {atm_label} at {obs_local_str} — NASA SVS Dial-a-Moon'
|
||||
)
|
||||
witness_text = f'sky-cam {cam} — clear at {obs_local_str} — NASA SVS Dial-a-Moon'
|
||||
|
||||
if dry_run:
|
||||
print(f'dry-run: would post with opacity={opacity:.2f} ({atm_label})')
|
||||
print(f'dry-run: would post using NASA image for {obs_utc.isoformat()}')
|
||||
return 0
|
||||
|
||||
return _render_and_post(
|
||||
phase, spec, target_utc, obs_utc, obs_utc.astimezone(tz),
|
||||
cam, dry_run, no_upload, out_path,
|
||||
witness_text=witness_text,
|
||||
east_frame_path=east_frame,
|
||||
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, atmosphere_opacity=0.0):
|
||||
dry_run, no_upload, out_path, witness_text):
|
||||
if out_path is None:
|
||||
out_dir = pathlib.Path(MOVIES_DIR) / cam / _output_subdir(phase)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -389,9 +355,7 @@ def _render_and_post(phase, spec, target_utc, when_utc, local_dt, cam,
|
||||
str(nasa_path), out_path,
|
||||
output_size=(OUT_W, OUT_H), moon_height_pct=MOON_PCT,
|
||||
caption=caption,
|
||||
east_frame_path=east_frame_path,
|
||||
atmosphere_opacity=atmosphere_opacity,
|
||||
atmosphere_blur=ATMOSPHERE_BLUR,
|
||||
when_utc=when_utc,
|
||||
)
|
||||
print(f'wrote {out_path}')
|
||||
|
||||
|
||||
+11
-40
@@ -492,20 +492,18 @@ MOON_TRACK_CRF=24 # output mp4 CRF
|
||||
MOON_TRACK_RETENTION_DAYS=90 # delete tracker mp4s older than this; 0 = forever
|
||||
|
||||
# Observation time ────────────────────────────────────────────────────────────
|
||||
# Local time used to select the east camera frame and the NASA Dial-a-Moon
|
||||
# render for each phase post. The script looks at east frames within
|
||||
# ±MOON_OBS_WINDOW_MIN of this time on the night of the exact phase event,
|
||||
# picks the one closest to the target time, and uses it to:
|
||||
# 1. Determine atmospheric conditions (clear / hazy / overcast)
|
||||
# 2. Set the atmosphere overlay opacity on the NASA moon image
|
||||
# 3. Round to the nearest hour for the NASA API call
|
||||
# Local time that defines the observation window: MOON_OBS_TIME_LOCAL to
|
||||
# MOON_OBS_TIME_LOCAL+1h. East frames in this window are checked for a clear
|
||||
# moon detection (quality >= MOON_MIN_QUALITY). If any frame qualifies, the
|
||||
# NASA Dial-a-Moon image for MOON_OBS_TIME_LOCAL UTC is fetched and posted.
|
||||
# If no frame shows a clear moon, the month is skipped entirely.
|
||||
#
|
||||
# 22:30 works well for full moon and first quarter (both visible after dark).
|
||||
# For third quarter (rises after midnight), consider 02:30 or leave at 22:30
|
||||
# and accept that the moon may be below the horizon — the script will warn
|
||||
# and post a clean NASA render instead.
|
||||
MOON_OBS_TIME_LOCAL=22:30
|
||||
MOON_OBS_WINDOW_MIN=30 # ±minutes around obs time to check east frames
|
||||
# 22:00 aligns directly with NASA's hourly renders. Full moon and third
|
||||
# quarter both rise after dark and are visible at this hour. First quarter
|
||||
# is up only until ~midnight from a new-moon start, so it may not be visible
|
||||
# at 22:00 depending on the exact date — set MOON_FIRST_QUARTER_ENABLED=false
|
||||
# if first-quarter posts are consistently missed.
|
||||
MOON_OBS_TIME_LOCAL=22:00
|
||||
|
||||
# Post delay ──────────────────────────────────────────────────────────────────
|
||||
# How many days after the exact phase to run the post. The post delay gives
|
||||
@@ -537,33 +535,6 @@ 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
|
||||
|
||||
# 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.
|
||||
#
|
||||
# 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)
|
||||
#
|
||||
# blur radius: 0 = auto (output_width / 10); set in px to override.
|
||||
#MOON_ATMOSPHERE_BLUR=0
|
||||
#
|
||||
# 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).
|
||||
|
||||
|
||||
+42
-112
@@ -3,25 +3,20 @@
|
||||
|
||||
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.
|
||||
1. Fetch the NASA SVS Dial-a-Moon render for 2026-04-29T22:00Z.
|
||||
2. Load the east camera frame (21-07-00.jpg) and check for a clear moon.
|
||||
3. If clear: render — NASA moon with parallactic angle rotation → test_composite_out.jpg.
|
||||
4. If not clear: exit with a message (no fallback image).
|
||||
|
||||
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
|
||||
@@ -30,67 +25,10 @@ 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.
|
||||
# Obs time: 22:00 UTC on 2026-04-29 — aligns directly with NASA hourly renders.
|
||||
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
|
||||
MIN_QUALITY = 0.55
|
||||
|
||||
|
||||
def main():
|
||||
@@ -103,59 +41,51 @@ def main():
|
||||
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}')
|
||||
# 1. Check east frame for clear moon detection
|
||||
print(f'checking moon in {EAST_FRAME.name} ...')
|
||||
try:
|
||||
from moon_detect import detect_moon
|
||||
det = detect_moon(str(EAST_FRAME))
|
||||
quality = det.quality if det is not None else None
|
||||
except Exception as e:
|
||||
print(f' detection failed: {e}', file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
if quality is None or quality < MIN_QUALITY:
|
||||
print(f' quality={quality} — no clear moon detection — skipping (no fallback)')
|
||||
sys.exit(0)
|
||||
|
||||
print(f' quality={quality:.3f} — clear shot confirmed')
|
||||
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}')
|
||||
# 2. Fetch NASA Dial-a-Moon
|
||||
print(f'fetching NASA Dial-a-Moon for {NASA_FETCH_UTC.strftime("%Y-%m-%dT%H:00Z")} ...')
|
||||
try:
|
||||
import moon_dialamoon
|
||||
nasa_path = moon_dialamoon.fetch_for_time(NASA_FETCH_UTC)
|
||||
print(f' cached at {nasa_path}')
|
||||
except Exception as e:
|
||||
print(f' NASA fetch failed: {e}', file=sys.stderr)
|
||||
sys.exit(3)
|
||||
print()
|
||||
|
||||
# 3. Render
|
||||
# 3. Render with parallactic angle rotation
|
||||
from moon_composite import render_phase_closeup
|
||||
caption = (
|
||||
f'Full Moon — April 2026 — '
|
||||
f'sky-cam east 2026-04-29 22:30 local — '
|
||||
f'sky-cam east 2026-04-29 22:00 UTC — '
|
||||
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')
|
||||
print(f'rendering {OUT_PATH.name} ...')
|
||||
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,
|
||||
when_utc=NASA_FETCH_UTC,
|
||||
)
|
||||
print(f'done -> {OUT_PATH}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user