composite: use east frame background with bilateral filter at 70% zoom

Switch monthly pipeline from render_phase_closeup (black background) to
composite_full_moon (east frame + NASA moon overlay) when an east frame
and detection are available.  Falls back to render_phase_closeup when
east verification is disabled.

Changes:
- composite_full_moon: bilateral filter (d=9, σ=75) on upscaled background
  to smooth pixelation artifacts before compositing the NASA moon
- composite_full_moon: caption now placed below the moon disk instead of
  overlapping it (paste_y + target_size + 12)
- run_phase: keep detection result (best_det) alongside best_frame
- _render_and_post: accept east_frame + detection, route to the right renderer
- Monthly pipeline uses moon_height_pct=0.70 for the east-frame composite

https://claude.ai/code/session_01JieSeNQZ3X6fhsb11YyJrK
This commit is contained in:
Claude
2026-05-03 13:17:36 +00:00
parent 54f342224a
commit 6b46eb5128
2 changed files with 31 additions and 10 deletions
+8 -2
View File
@@ -191,6 +191,11 @@ def composite_full_moon(
crop = src.crop((left, top, left + crop_w, top + crop_h))
bg = crop.resize(output_size, LANCZOS)
# ── Smooth upscaling artifacts in the sky background ──
import cv2
bg_arr = cv2.bilateralFilter(np.array(bg), d=9, sigmaColor=75, sigmaSpace=75)
bg = Image.fromarray(bg_arr)
# ── Where is the moon's center within the upscaled background? ──
moon_x_in_crop = cx - left
moon_y_in_crop = cy - top
@@ -230,10 +235,11 @@ def composite_full_moon(
paste_y = max(0, min(paste_y, output_size[1] - target_size))
bg.paste(ref_rot, (paste_x, paste_y), mask)
# ── Caption ──
# ── Caption — below the moon disk, not overlapping it ──
if caption:
draw = ImageDraw.Draw(bg)
_draw_caption(draw, caption, (22, output_size[1] - 48), output_size[0])
caption_y = min(paste_y + target_size + 12, output_size[1] - 30)
_draw_caption(draw, caption, (22, caption_y), output_size[0])
Path(out_path).parent.mkdir(parents=True, exist_ok=True)
bg.save(out_path, quality=92)
+23 -8
View File
@@ -337,11 +337,13 @@ def run_phase(phase: str, target_utc: datetime | None, cam: str,
dark_frames.sort(key=lambda x: abs(x[1] - target_utc))
best_frame: str | None = None
best_det = None
best_dt: datetime | None = None
for fpath, fdt in dark_frames:
det = detect_moon(fpath)
if det is not None and det.quality >= MIN_QUALITY:
best_frame = fpath
best_det = det
best_dt = fdt
offset_min = int((fdt - target_utc).total_seconds() / 60)
print(f'best frame: {pathlib.Path(fpath).name} quality={det.quality:.3f} '
@@ -368,11 +370,14 @@ def run_phase(phase: str, target_utc: datetime | None, cam: str,
phase, spec, target_utc, best_dt,
cam, dry_run, no_upload, out_path,
witness_text=f'sky-cam {cam}{illum_pct}% lit — {_format_local(best_dt)}',
east_frame=best_frame,
detection=best_det,
)
def _render_and_post(phase, spec, target_utc, when_utc, cam,
dry_run, no_upload, out_path, witness_text):
dry_run, no_upload, out_path, witness_text,
east_frame=None, detection=None):
if out_path is None:
out_dir = pathlib.Path(MOVIES_DIR) / cam / _output_subdir(phase)
out_dir.mkdir(parents=True, exist_ok=True)
@@ -393,17 +398,27 @@ def _render_and_post(phase, spec, target_utc, when_utc, cam,
print(f'dial-a-moon: {nasa_path}')
illum_pct = round(_mp.illumination(when_utc) * 100)
from moon_composite import render_phase_closeup
caption = (
f"{spec['label']}{illum_pct}% lit — {target_utc.strftime('%B %Y')}"
f"{witness_text} — NASA SVS Dial-a-Moon"
)
render_phase_closeup(
str(nasa_path), out_path,
output_size=(OUT_W, OUT_H), moon_height_pct=MOON_PCT,
caption=caption,
when_utc=when_utc,
)
if east_frame is not None and detection is not None:
from moon_composite import composite_full_moon
composite_full_moon(
east_frame, detection, when_utc,
str(nasa_path), out_path,
output_size=(OUT_W, OUT_H),
moon_height_pct=0.70,
caption=caption,
)
else:
from moon_composite import render_phase_closeup
render_phase_closeup(
str(nasa_path), out_path,
output_size=(OUT_W, OUT_H), moon_height_pct=MOON_PCT,
caption=caption,
when_utc=when_utc,
)
print(f'wrote {out_path}')
if no_upload: