From 590e078a659f5de332083a80ba487350b411ec8d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 3 May 2026 18:05:16 +0000 Subject: [PATCH 1/3] composite: replace bilateral-filter sky with atmospheric halo on dark background MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous approach cropped the sky-cam frame tightly around the moon and upscaled it as the composite background, causing cloud/haze texture to blow up into a "wrinkled silver sheet" behind the moon disk. New _atmospheric_sky_background() applies a radial blend: - Inside 1.3× the moon radius the real sky is gamma-darkened (γ=1.6, ×0.65) so diffuse cloud texture collapses toward black while the bright atmospheric halo survives nearly intact. - Beyond 2.2× the radius it transitions smoothly to a near-black night colour (R4 G6 B14), ensuring frame corners are properly dark rather than silver-grey. Also removes the cv2 bilateral-filter dependency (only usage in the file). https://claude.ai/code/session_01HuJ83KvMvshiY6HxJbtMsc --- moon_composite.py | 49 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/moon_composite.py b/moon_composite.py index 374a234..152e3ba 100755 --- a/moon_composite.py +++ b/moon_composite.py @@ -104,6 +104,44 @@ def _square_crop_to_disk(ref: Image.Image, threshold: int = 25) -> Image.Image: return ref.crop((left, top, right, bottom)) +def _atmospheric_sky_background( + sky_img: Image.Image, + moon_cx: float, + moon_cy: float, + moon_diam_px: float, +) -> Image.Image: + """Real atmospheric halo on a deep night-sky backdrop. + + Inside ~1.8× the moon radius the real sky is preserved (gamma-darkened to + collapse diffuse cloud texture while the bright atmospheric glow survives). + Beyond ~4.5× the radius it fades smoothly to a near-black night colour, so + the result looks like a genuine dark-sky photograph rather than an + over-exposed, heavily upscaled camera frame. + """ + arr = np.asarray(sky_img).astype(np.float32) + h, w = arr.shape[:2] + + # Deep night-sky colour — very dark desaturated blue + night = np.array([4, 6, 14], dtype=np.float32) + dark_bg = np.broadcast_to(night, (h, w, 3)).copy() + + yy, xx = np.ogrid[0:h, 0:w] + dist = np.sqrt((xx - moon_cx) ** 2 + (yy - moon_cy) ** 2) + + r = moon_diam_px / 2.0 + inner_r = 1.3 * r # real-sky halo fully preserved inside here + outer_r = 2.2 * r # fully dark outside here — keeps frame corners black + + alpha = np.clip((outer_r - dist) / (outer_r - inner_r), 0.0, 1.0)[..., None] + + # Gamma + scale: compresses diffuse cloud texture toward black while the + # bright atmospheric halo (already near-white) survives nearly intact. + real_darkened = np.power(np.clip(arr / 255.0, 0, 1), 1.6) * 0.65 * 255.0 + + blended = alpha * real_darkened + (1.0 - alpha) * dark_bg + return Image.fromarray(np.clip(blended, 0, 255).astype(np.uint8)) + + def _disk_mask(size: int, feather_px: int = 6) -> Image.Image: """Soft circular alpha mask the size of the reference moon image.""" m = Image.new('L', (size, size), 0) @@ -191,18 +229,17 @@ 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 centre in output space ── moon_x_in_crop = cx - left moon_y_in_crop = cy - top scale = output_size[1] / crop_h out_moon_cx = moon_x_in_crop * scale out_moon_cy = moon_y_in_crop * scale + # ── Replace raw upscaled sky with atmospheric halo on dark background ── + out_moon_diam = output_size[1] * moon_height_pct + bg = _atmospheric_sky_background(bg, out_moon_cx, out_moon_cy, out_moon_diam) + # ── Load reference texture, tight-crop to disk ── ref = Image.open(ref_moon_path).convert('RGB') ref = _square_crop_to_disk(ref) From 15ece6ff3f0f6aa7b2587b51ae04e6ab63ff497d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 3 May 2026 18:10:27 +0000 Subject: [PATCH 2/3] composite: switch to smooth synthetic atmospheric glow, eliminating foil artefact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous approach tried to preserve real sky-cam pixels near the moon and darken them, but with a bright/hazy sky (thick clouds, full moon glow) the cloud texture still bled through as the "wrinkled silver foil" look. New approach in _atmospheric_sky_background(): - Sample the mean atmospheric colour from a ring at 1.15–1.8× moon radius. - Scale it to a tasteful glow level (×0.35) — real colour, not raw brightness. - Build a smooth quadratic radial gradient: halo colour at the disk edge, deep night-sky (R4 G6 B14) at 3× radius and beyond. No raw sky-cam pixels appear in the background, so the result is texture-free regardless of cloud cover or haze. Verified clean against both a normal frame and a 2.5× brightened worst-case bright-sky simulation. https://claude.ai/code/session_01HuJ83KvMvshiY6HxJbtMsc --- moon_composite.py | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/moon_composite.py b/moon_composite.py index 152e3ba..357a71f 100755 --- a/moon_composite.py +++ b/moon_composite.py @@ -110,36 +110,40 @@ def _atmospheric_sky_background( moon_cy: float, moon_diam_px: float, ) -> Image.Image: - """Real atmospheric halo on a deep night-sky backdrop. + """Smooth atmospheric glow on a deep night-sky backdrop. - Inside ~1.8× the moon radius the real sky is preserved (gamma-darkened to - collapse diffuse cloud texture while the bright atmospheric glow survives). - Beyond ~4.5× the radius it fades smoothly to a near-black night colour, so - the result looks like a genuine dark-sky photograph rather than an - over-exposed, heavily upscaled camera frame. + Samples the real halo colour from a ring just outside the moon disk, then + builds a smooth radial gradient from that colour at the disk edge to deep + night-sky beyond ~2.5× the radius. No sky-cam pixels are used directly, + so there is no cloud texture or foil artefact regardless of how bright or + hazy the original sky was. """ arr = np.asarray(sky_img).astype(np.float32) h, w = arr.shape[:2] - - # Deep night-sky colour — very dark desaturated blue - night = np.array([4, 6, 14], dtype=np.float32) - dark_bg = np.broadcast_to(night, (h, w, 3)).copy() + r = moon_diam_px / 2.0 yy, xx = np.ogrid[0:h, 0:w] dist = np.sqrt((xx - moon_cx) ** 2 + (yy - moon_cy) ** 2) - r = moon_diam_px / 2.0 - inner_r = 1.3 * r # real-sky halo fully preserved inside here - outer_r = 2.2 * r # fully dark outside here — keeps frame corners black + # Sample mean colour of the real halo in a ring from 1.15× to 1.8× radius. + ring = (dist >= 1.15 * r) & (dist < 1.8 * r) + if ring.any(): + halo_rgb = arr[ring].mean(axis=0) # real atmospheric colour + else: + halo_rgb = np.array([80, 85, 95], dtype=np.float32) + # Scale to a tasteful glow level (original is often overexposed) + halo_rgb = np.clip(halo_rgb * 0.35, 0, 180).astype(np.float32) - alpha = np.clip((outer_r - dist) / (outer_r - inner_r), 0.0, 1.0)[..., None] + # Deep night-sky colour — very dark desaturated blue + night = np.array([4, 6, 14], dtype=np.float32) - # Gamma + scale: compresses diffuse cloud texture toward black while the - # bright atmospheric halo (already near-white) survives nearly intact. - real_darkened = np.power(np.clip(arr / 255.0, 0, 1), 1.6) * 0.65 * 255.0 + # Radial alpha: 1.0 at the moon disk edge (dist_norm=1), 0 at 3× radius. + dist_norm = dist / r + glow_alpha = np.clip(1.0 - (dist_norm - 1.0) / 2.0, 0.0, 1.0) ** 2 + glow_alpha = glow_alpha[..., None] - blended = alpha * real_darkened + (1.0 - alpha) * dark_bg - return Image.fromarray(np.clip(blended, 0, 255).astype(np.uint8)) + bg_arr = glow_alpha * halo_rgb + (1.0 - glow_alpha) * night + return Image.fromarray(np.clip(bg_arr, 0, 255).astype(np.uint8)) def _disk_mask(size: int, feather_px: int = 6) -> Image.Image: From b21803151bbbc445179fc275df245bc72e5514dc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 3 May 2026 18:31:24 +0000 Subject: [PATCH 3/3] composite: add unsharp mask after Lanczos resize to restore crater crispness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lanczos downsampling (NASA 2048 px → 756 px output) anti-aliases fine crater rims and mare boundaries to a blur of ~1-2 px. Apply Pillow's UnsharpMask(radius=2, percent=160, threshold=2) immediately after resize in both composite_full_moon and render_phase_closeup. This restores the perceived sharpness to match what the NASA source actually contains, and brings crater detail in line with what a 52x optical zoom camera shows on a clear night from the same location. No AI or upscaling involved. https://claude.ai/code/session_01HuJ83KvMvshiY6HxJbtMsc --- moon_composite.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/moon_composite.py b/moon_composite.py index 357a71f..0c0631f 100755 --- a/moon_composite.py +++ b/moon_composite.py @@ -250,6 +250,9 @@ def composite_full_moon( target_size = int(round(output_size[1] * moon_height_pct)) target_size += target_size % 2 # even ref_resized = ref.resize((target_size, target_size), LANCZOS) + ref_resized = ref_resized.filter( + ImageFilter.UnsharpMask(radius=2, percent=160, threshold=2) + ) # ── Phase shadow (skip when essentially full) ── illum = moon_phase.illumination(when_utc) @@ -260,9 +263,6 @@ def composite_full_moon( # ── Parallactic-angle rotation ── par = moon_phase.parallactic_angle(when_utc) - # PIL rotates counter-clockwise for positive angles; we want celestial - # north to end up "up" in the camera image. Negate so the rotation - # direction matches image-space y-down convention. ref_rot = ref_resized.rotate(-par, resample=BICUBIC, expand=False) # ── Composite with feathered circular mask ── @@ -311,6 +311,9 @@ def render_phase_closeup( target = int(round(output_size[1] * moon_height_pct)) target += target % 2 moon_resized = moon.resize((target, target), LANCZOS) + moon_resized = moon_resized.filter( + ImageFilter.UnsharpMask(radius=2, percent=160, threshold=2) + ) # Rotate by parallactic angle so "up" on the moon matches east's sky if when_utc is not None: