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)