Merge pull request #72 from outis1one/claude/moon-photography-enhancement-dD9R6
Claude/moon photography enhancement d d9 r6
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
__pycache__/
|
||||
.env
|
||||
sunrise-sounds/
|
||||
*_debug_standard.jpg
|
||||
*_debug_crescent.jpg
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
"""test_crescent_detect.py — test crescent moon detection on a camera frame.
|
||||
|
||||
Runs detect_moon() twice on the same image:
|
||||
1. Standard parameters (full/quarter moon defaults)
|
||||
2. Crescent parameters (relaxed roundness, lower brightness threshold)
|
||||
|
||||
Saves annotated debug images so you can see exactly what each pass found.
|
||||
Optionally renders the full composite if detection succeeds and a reference
|
||||
moon image (or cached NASA dial-a-moon) is available.
|
||||
|
||||
Usage:
|
||||
|
||||
# Basic detection test against any east camera frame:
|
||||
python3 test_crescent_detect.py PATH/TO/FRAME.jpg
|
||||
|
||||
# Also render the composite (needs ref moon image):
|
||||
python3 test_crescent_detect.py PATH/TO/FRAME.jpg --ref PATH/TO/MOON.jpg
|
||||
|
||||
# Specify the capture time for parallactic-angle rotation:
|
||||
python3 test_crescent_detect.py PATH/TO/FRAME.jpg --ref PATH/TO/MOON.jpg \\
|
||||
--when 2026-05-08T05:30:00Z --phase waning-crescent
|
||||
|
||||
# Quick self-test using the bundled east frame (no crescent, but confirms
|
||||
# the standard detector still works after the crescent changes):
|
||||
python3 test_crescent_detect.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HERE = pathlib.Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
# ── Default self-test frame ───────────────────────────────────────────────────
|
||||
_DEFAULT_FRAME = HERE / '21-07-00.jpg'
|
||||
|
||||
# ── Crescent thresholds (mirrors moon_phase_monthly.CRESCENT_* defaults) ─────
|
||||
CRESCENT_SATURATED_THR = 180
|
||||
CRESCENT_MIN_ROUNDNESS = 0.15
|
||||
CRESCENT_MAX_HALO_RATIO = 12.0
|
||||
CRESCENT_MIN_QUALITY = 0.35
|
||||
|
||||
|
||||
def _label(det, min_q: float) -> str:
|
||||
if det is None:
|
||||
return 'NOT DETECTED'
|
||||
verdict = 'PASS' if det.quality >= min_q else f'FAIL (quality {det.quality:.3f} < {min_q})'
|
||||
return (
|
||||
f'{verdict} cx={det.centroid_xy[0]:.0f} cy={det.centroid_xy[1]:.0f} '
|
||||
f'diam={det.diameter_px:.0f}px roundness={det.roundness:.2f} '
|
||||
f'halo_ratio={det.halo_ratio:.1f} quality={det.quality:.3f}'
|
||||
)
|
||||
|
||||
|
||||
def run(frame: str, ref_moon: str | None, when_utc: datetime | None,
|
||||
phase: str, out_dir: pathlib.Path) -> int:
|
||||
from moon_detect import detect_moon, _draw_debug
|
||||
import moon_phase_monthly as mpm
|
||||
|
||||
frame_p = pathlib.Path(frame)
|
||||
if not frame_p.exists():
|
||||
print(f'ERROR: frame not found: {frame}', file=sys.stderr)
|
||||
return 1
|
||||
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
stem = frame_p.stem
|
||||
|
||||
print(f'frame : {frame_p}')
|
||||
print(f'phase : {phase}')
|
||||
print()
|
||||
|
||||
# ── Pass 1: standard detection ────────────────────────────────────────────
|
||||
det_std = detect_moon(str(frame_p))
|
||||
print(f'standard (thr=240 round≥0.55 halo≤6.0) → {_label(det_std, mpm.MIN_QUALITY)}')
|
||||
debug_std = out_dir / f'{stem}_debug_standard.jpg'
|
||||
_draw_debug(str(frame_p), det_std, str(debug_std))
|
||||
print(f' debug → {debug_std}')
|
||||
|
||||
print()
|
||||
|
||||
# ── Pass 2: crescent detection ────────────────────────────────────────────
|
||||
det_cre = detect_moon(
|
||||
str(frame_p),
|
||||
saturated_threshold=CRESCENT_SATURATED_THR,
|
||||
min_roundness=CRESCENT_MIN_ROUNDNESS,
|
||||
max_halo_ratio=CRESCENT_MAX_HALO_RATIO,
|
||||
)
|
||||
print(f'crescent (thr=180 round≥0.15 halo≤12.0) → {_label(det_cre, CRESCENT_MIN_QUALITY)}')
|
||||
debug_cre = out_dir / f'{stem}_debug_crescent.jpg'
|
||||
_draw_debug(str(frame_p), det_cre, str(debug_cre))
|
||||
print(f' debug → {debug_cre}')
|
||||
|
||||
# ── Optional composite render ─────────────────────────────────────────────
|
||||
det_for_render = det_cre if phase in ('waxing-crescent', 'waning-crescent') else det_std
|
||||
min_q = CRESCENT_MIN_QUALITY if phase in ('waxing-crescent', 'waning-crescent') else mpm.MIN_QUALITY
|
||||
|
||||
if ref_moon and det_for_render is not None and det_for_render.quality >= min_q:
|
||||
print()
|
||||
if when_utc is None:
|
||||
print('note: --when not supplied; skipping parallactic rotation')
|
||||
when_utc = datetime.now(timezone.utc)
|
||||
|
||||
# Monkeypatch moon_phase if skyfield is unavailable
|
||||
try:
|
||||
import moon_phase as _mp
|
||||
_mp.illumination(when_utc) # probe — raises if no ephemeris
|
||||
except Exception:
|
||||
print('skyfield/ephemeris not available — using stub moon_phase values')
|
||||
import moon_phase as _mp
|
||||
illum = {'waxing-crescent': 0.28, 'waning-crescent': 0.28,
|
||||
'full': 1.0, 'first-quarter': 0.50, 'third-quarter': 0.50}
|
||||
_mp.illumination = lambda w: illum.get(phase, 0.50)
|
||||
_mp.phase_angle = lambda w: {'waxing-crescent': 110,
|
||||
'waning-crescent': 110,
|
||||
'full': 0, 'first-quarter': 90,
|
||||
'third-quarter': 90}.get(phase, 90)
|
||||
_mp.waxing = lambda w: phase == 'waxing-crescent'
|
||||
_mp.parallactic_angle = lambda w, **kw: 0.0
|
||||
|
||||
from moon_composite import composite_full_moon
|
||||
out_img = out_dir / f'{stem}_{phase}_composite.jpg'
|
||||
composite_full_moon(
|
||||
source_jpg=str(frame_p),
|
||||
detection=det_for_render,
|
||||
when_utc=when_utc,
|
||||
ref_moon_path=ref_moon,
|
||||
out_path=str(out_img),
|
||||
output_size=(1920, 1080),
|
||||
moon_height_pct=0.70,
|
||||
caption=(
|
||||
f'{phase.replace("-", " ").title()} — '
|
||||
f'{when_utc.strftime("%B %Y")} — sky-cam east — NASA SVS Dial-a-Moon'
|
||||
),
|
||||
)
|
||||
print(f'composite → {out_img}')
|
||||
elif ref_moon:
|
||||
print()
|
||||
print('composite skipped — detection did not pass quality threshold')
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
p.add_argument('frame', nargs='?', default=str(_DEFAULT_FRAME),
|
||||
help='east camera JPEG to test (default: bundled 21-07-00.jpg)')
|
||||
p.add_argument('--ref', metavar='MOON_JPG',
|
||||
help='reference moon image for composite render '
|
||||
'(NASA dial-a-moon PNG or any moon photo)')
|
||||
p.add_argument('--when', metavar='ISO_UTC',
|
||||
help='capture time, e.g. 2026-05-08T05:30:00Z '
|
||||
'(used for parallactic rotation; defaults to now)')
|
||||
p.add_argument('--phase',
|
||||
choices=['waxing-crescent', 'waning-crescent',
|
||||
'full', 'first-quarter', 'third-quarter'],
|
||||
default='waning-crescent',
|
||||
help='which phase to simulate (affects detection thresholds '
|
||||
'and composite labelling, default: waning-crescent)')
|
||||
p.add_argument('--out-dir', metavar='DIR', default=str(HERE),
|
||||
help='directory for debug and composite output (default: sky-cam dir)')
|
||||
args = p.parse_args()
|
||||
|
||||
when = None
|
||||
if args.when:
|
||||
when = datetime.fromisoformat(args.when.replace('Z', '+00:00'))
|
||||
if when.tzinfo is None:
|
||||
when = when.replace(tzinfo=timezone.utc)
|
||||
|
||||
return run(
|
||||
frame=args.frame,
|
||||
ref_moon=args.ref,
|
||||
when_utc=when,
|
||||
phase=args.phase,
|
||||
out_dir=pathlib.Path(args.out_dir),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user