Add waxing/waning crescent phases; fix phase-shadow double-application
moon_detect.py - detect_moon() accepts saturated_threshold / min_roundness / max_halo_ratio so crescent phases (dim, non-circular arc) can be detected with relaxed thresholds without changing defaults for full/quarter detection. moon_phase.py - crescent_times_in_range(): 2-hour scan returning the UTC moment illumination crosses a target fraction (default 28%) on the waxing or waning side. Used for scheduling just like phase_events_in_range() for named phases. moon_composite.py - Remove _apply_phase_shadow() from composite_full_moon. NASA SVS Dial-a-Moon renders already include the correct terminator, libration and earthshine for the exact hour; applying the shadow on top double-darkened the unlit limb on every non-full phase. The function stays available for callers that need it. moon_phase_monthly.py - Add waxing-crescent (🌒) and waning-crescent (🌘) to PHASE_SPEC. Timing via crescent_times_in_range(); scheduling identical to other phases. - CRESCENT_* tuning constants (all overridable from sky-cam.conf): MOON_CRESCENT_TARGET_ILLUM default 0.28 MOON_CRESCENT_MIN_QUALITY default 0.35 MOON_CRESCENT_SATURATED_THR default 180 MOON_CRESCENT_MIN_ROUNDNESS default 0.15 MOON_CRESCENT_MAX_HALO_RATIO default 12.0 - run_phase() and auto_run() branch on spec['crescent'] to use crescent timing and detection params. - Atmospheric background naturally captures twilight colours (dawn for waning crescent, dusk/dawn for waxing) from the real east frame. https://claude.ai/code/session_01HuJ83KvMvshiY6HxJbtMsc
This commit is contained in:
+21
-9
@@ -78,12 +78,14 @@ def _grayscale_array(image_path: str) -> np.ndarray:
|
||||
return np.asarray(im)
|
||||
|
||||
|
||||
def _largest_round_blob(mask: np.ndarray) -> tuple[int, np.ndarray, np.ndarray] | None:
|
||||
def _largest_round_blob(
|
||||
mask: np.ndarray,
|
||||
min_roundness: float = MIN_ROUNDNESS,
|
||||
) -> tuple[int, np.ndarray, np.ndarray] | None:
|
||||
labels, n = ndimage.label(mask)
|
||||
if n == 0:
|
||||
return None
|
||||
sizes = ndimage.sum(mask, labels, range(1, n + 1))
|
||||
# Sort blobs by size descending; check roundness on the top few
|
||||
order = np.argsort(sizes)[::-1]
|
||||
for idx in order[:8]:
|
||||
label_id = idx + 1
|
||||
@@ -97,23 +99,33 @@ def _largest_round_blob(mask: np.ndarray) -> tuple[int, np.ndarray, np.ndarray]
|
||||
continue
|
||||
radius = diam / 2.0
|
||||
roundness = len(xs) / (math.pi * radius * radius)
|
||||
if roundness < MIN_ROUNDNESS:
|
||||
if roundness < min_roundness:
|
||||
continue
|
||||
return label_id, ys, xs
|
||||
return None
|
||||
|
||||
|
||||
def detect_moon(image_path: str) -> MoonDetection | None:
|
||||
"""Return MoonDetection or None if no acceptable moon is found."""
|
||||
def detect_moon(
|
||||
image_path: str,
|
||||
saturated_threshold: int = SATURATED_THRESHOLD,
|
||||
min_roundness: float = MIN_ROUNDNESS,
|
||||
max_halo_ratio: float = MAX_HALO_RATIO,
|
||||
) -> MoonDetection | None:
|
||||
"""Return MoonDetection or None if no acceptable moon is found.
|
||||
|
||||
saturated_threshold / min_roundness / max_halo_ratio can be relaxed for
|
||||
crescent phases, which produce a dim, non-circular arc rather than a bright
|
||||
near-perfect disk.
|
||||
"""
|
||||
a = _grayscale_array(image_path)
|
||||
if OVERLAY_PX > 0:
|
||||
a = a.copy()
|
||||
a[-OVERLAY_PX:, :] = 0
|
||||
mask = a >= SATURATED_THRESHOLD
|
||||
mask = a >= saturated_threshold
|
||||
if not mask.any():
|
||||
return None
|
||||
|
||||
found = _largest_round_blob(mask)
|
||||
found = _largest_round_blob(mask, min_roundness)
|
||||
if found is None:
|
||||
return None
|
||||
_, ys, xs = found
|
||||
@@ -153,12 +165,12 @@ def detect_moon(image_path: str) -> MoonDetection | None:
|
||||
yh, xh = np.where(halo_labels == halo_id)
|
||||
halo_radius = max(xh.max() - xh.min(), yh.max() - yh.min()) / 2.0
|
||||
halo_ratio = halo_radius / radius if radius > 0 else 1.0
|
||||
if halo_ratio > MAX_HALO_RATIO:
|
||||
if halo_ratio > max_halo_ratio:
|
||||
return None # too much glow → probably thick cloud cover
|
||||
|
||||
# Quality score: roundness (0..1), low halo (1 = clear, 0 = thick cloud),
|
||||
# isolation factor (1 if very isolated, less if close to other lights).
|
||||
halo_clean = max(0.0, min(1.0, (MAX_HALO_RATIO - halo_ratio) / (MAX_HALO_RATIO - 1.5)))
|
||||
halo_clean = max(0.0, min(1.0, (max_halo_ratio - halo_ratio) / (max_halo_ratio - 1.5)))
|
||||
iso_factor = 1.0 if isolation == float('inf') else min(1.0, isolation / 600.0)
|
||||
quality = 0.5 * roundness + 0.35 * halo_clean + 0.15 * iso_factor
|
||||
|
||||
|
||||
Reference in New Issue
Block a user