composite: switch to smooth synthetic atmospheric glow, eliminating foil artefact
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
This commit is contained in:
+23
-19
@@ -110,36 +110,40 @@ def _atmospheric_sky_background(
|
|||||||
moon_cy: float,
|
moon_cy: float,
|
||||||
moon_diam_px: float,
|
moon_diam_px: float,
|
||||||
) -> Image.Image:
|
) -> 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
|
Samples the real halo colour from a ring just outside the moon disk, then
|
||||||
collapse diffuse cloud texture while the bright atmospheric glow survives).
|
builds a smooth radial gradient from that colour at the disk edge to deep
|
||||||
Beyond ~4.5× the radius it fades smoothly to a near-black night colour, so
|
night-sky beyond ~2.5× the radius. No sky-cam pixels are used directly,
|
||||||
the result looks like a genuine dark-sky photograph rather than an
|
so there is no cloud texture or foil artefact regardless of how bright or
|
||||||
over-exposed, heavily upscaled camera frame.
|
hazy the original sky was.
|
||||||
"""
|
"""
|
||||||
arr = np.asarray(sky_img).astype(np.float32)
|
arr = np.asarray(sky_img).astype(np.float32)
|
||||||
h, w = arr.shape[:2]
|
h, w = arr.shape[:2]
|
||||||
|
r = moon_diam_px / 2.0
|
||||||
# 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]
|
yy, xx = np.ogrid[0:h, 0:w]
|
||||||
dist = np.sqrt((xx - moon_cx) ** 2 + (yy - moon_cy) ** 2)
|
dist = np.sqrt((xx - moon_cx) ** 2 + (yy - moon_cy) ** 2)
|
||||||
|
|
||||||
r = moon_diam_px / 2.0
|
# Sample mean colour of the real halo in a ring from 1.15× to 1.8× radius.
|
||||||
inner_r = 1.3 * r # real-sky halo fully preserved inside here
|
ring = (dist >= 1.15 * r) & (dist < 1.8 * r)
|
||||||
outer_r = 2.2 * r # fully dark outside here — keeps frame corners black
|
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
|
# Radial alpha: 1.0 at the moon disk edge (dist_norm=1), 0 at 3× radius.
|
||||||
# bright atmospheric halo (already near-white) survives nearly intact.
|
dist_norm = dist / r
|
||||||
real_darkened = np.power(np.clip(arr / 255.0, 0, 1), 1.6) * 0.65 * 255.0
|
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
|
bg_arr = glow_alpha * halo_rgb + (1.0 - glow_alpha) * night
|
||||||
return Image.fromarray(np.clip(blended, 0, 255).astype(np.uint8))
|
return Image.fromarray(np.clip(bg_arr, 0, 255).astype(np.uint8))
|
||||||
|
|
||||||
|
|
||||||
def _disk_mask(size: int, feather_px: int = 6) -> Image.Image:
|
def _disk_mask(size: int, feather_px: int = 6) -> Image.Image:
|
||||||
|
|||||||
Reference in New Issue
Block a user