composite: replace bilateral-filter sky with atmospheric halo on dark background
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
This commit is contained in:
+43
-6
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user