Merge pull request #70 from outis1one/claude/moon-photography-enhancement-dD9R6
Claude/moon photography enhancement d d9 r6
This commit is contained in:
+53
-9
@@ -104,6 +104,48 @@ 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:
|
||||
"""Smooth atmospheric glow on a deep night-sky backdrop.
|
||||
|
||||
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]
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
# Deep night-sky colour — very dark desaturated blue
|
||||
night = np.array([4, 6, 14], dtype=np.float32)
|
||||
|
||||
# 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]
|
||||
|
||||
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:
|
||||
"""Soft circular alpha mask the size of the reference moon image."""
|
||||
m = Image.new('L', (size, size), 0)
|
||||
@@ -191,24 +233,26 @@ 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)
|
||||
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)
|
||||
@@ -219,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 ──
|
||||
@@ -270,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:
|
||||
|
||||
Reference in New Issue
Block a user