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])
|
||||
|
||||
Reference in New Issue
Block a user