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__/
|
__pycache__/
|
||||||
.env
|
.env
|
||||||
sunrise-sounds/
|
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