Two new jobs operate on the SUNRISE_CAM, sharing three Python helpers (moon_phase, moon_detect, moon_composite) and one cached lunar texture: - moon-track.sh: nightly batch detects the moon in each east frame, crops a 480x480 box around it, stitches into an mp4 that holds the moon roughly centred while clouds and stars drift past. - moon-phase-monthly.sh: daily check that runs whichever phase composite is due that day. Handles full moon (posts D+3), first quarter (D+2, best-effort due to daytime-only geometry from east), and third quarter (D+2). Picks the frame closest in time to the exact phase moment that meets quality / altitude / illumination thresholds, then composites the cached lunar texture into it -- sky/halo/parallactic-angle/timing real from east, surface detail borrowed from the reference image. Honest-by-design: a 38-px white blob from a wide-field IP camera cannot be enhanced into crater detail by software. The composite makes the borrowing explicit and constrains everything else (when, where, sky, orientation) to match what east actually saw. install.sh now downloads the skyfield ephemeris (de421.bsp) and the default lunar reference (Wikipedia CC BY-SA full-moon photo) on first run. Both can be overridden via .env. https://claude.ai/code/session_015PBVDESC3KLMbq1LpA6qLn
107 lines
3.2 KiB
Bash
Executable File
107 lines
3.2 KiB
Bash
Executable File
#!/bin/bash
|
|
# moon-track.sh — nightly: detect the moon in each east night frame, crop a
|
|
# fixed-size box around it, stitch the cropped sequence into an mp4 that holds
|
|
# the moon roughly centred while clouds, halo and stars drift past.
|
|
#
|
|
# Run once per day from systemd around 02:00, after the night the moon was up.
|
|
# Processes YESTERDAY's frames by default; pass YYYY-MM-DD to back-fill.
|
|
#
|
|
# Output: $MOVIES_DIR/<cam>/moon-track/YYYY/YYYY-MM-DD-moon-track.mp4
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
|
|
source "$SCRIPT_DIR/sky-cam.conf"
|
|
export TZ="$TIMEZONE"
|
|
|
|
CAM_NAME="${1:-${SUNRISE_CAM:-east}}"
|
|
DATE_ARG="${2:-}"
|
|
if [ -n "$DATE_ARG" ]; then
|
|
target_date=$(date --date="$DATE_ARG" +%Y-%m-%d)
|
|
else
|
|
target_date=$(date --date="yesterday" +%Y-%m-%d)
|
|
fi
|
|
|
|
if [ "${MOON_TRACK_ENABLED:-true}" != "true" ]; then
|
|
echo "MOON_TRACK_ENABLED=false — skipping"
|
|
exit 0
|
|
fi
|
|
|
|
image_dir="$BASE_DIR/$CAM_NAME/$target_date"
|
|
if [ ! -d "$image_dir" ]; then
|
|
echo "no images for $CAM_NAME on $target_date — skipping"
|
|
exit 0
|
|
fi
|
|
|
|
year="${target_date:0:4}"
|
|
out_dir="$MOVIES_DIR/$CAM_NAME/moon-track/$year"
|
|
mkdir -p "$out_dir"
|
|
final="$out_dir/${target_date}-moon-track.mp4"
|
|
|
|
# Crop size around the detected moon, in source pixels. At 4K with a ~38 px
|
|
# moon, 480 px gives a comfortable surround with halo and any clouds visible.
|
|
CROP_PX="${MOON_TRACK_CROP_PX:-480}"
|
|
FPS="${MOON_TRACK_FPS:-12}"
|
|
CRF="${MOON_TRACK_CRF:-24}"
|
|
|
|
work_dir=$(mktemp -d)
|
|
trap 'rm -rf "$work_dir"' EXIT
|
|
|
|
export SCRIPT_DIR
|
|
echo "Scanning $image_dir for night frames with the moon visible..."
|
|
python3 - "$image_dir" "$work_dir" "$CROP_PX" <<'PY'
|
|
import os, sys
|
|
sys.path.insert(0, os.environ['SCRIPT_DIR'])
|
|
from PIL import Image
|
|
from moon_detect import detect_moon
|
|
|
|
image_dir = sys.argv[1]
|
|
work_dir = sys.argv[2]
|
|
crop_px = int(sys.argv[3])
|
|
half = crop_px // 2
|
|
|
|
frames = sorted(f for f in os.listdir(image_dir) if f.endswith('.jpg'))
|
|
n = len(frames)
|
|
written = 0
|
|
for i, name in enumerate(frames):
|
|
p = os.path.join(image_dir, name)
|
|
det = detect_moon(p)
|
|
if det is None:
|
|
continue
|
|
cx, cy = det.centroid_xy
|
|
im = Image.open(p)
|
|
w, h = im.size
|
|
left = max(0, min(int(cx - half), w - crop_px))
|
|
top = max(0, min(int(cy - half), h - crop_px))
|
|
crop = im.crop((left, top, left + crop_px, top + crop_px))
|
|
out = os.path.join(work_dir, f'{written:06d}.jpg')
|
|
crop.save(out, quality=88)
|
|
written += 1
|
|
if (i + 1) % 200 == 0 or i + 1 == n:
|
|
print(f' scanned {i + 1}/{n} frames, kept {written}', flush=True)
|
|
print(f'moon-detected frames: {written}')
|
|
PY
|
|
|
|
count=$(find "$work_dir" -maxdepth 1 -name '*.jpg' | wc -l)
|
|
if [ "$count" -lt 10 ]; then
|
|
echo "only $count moon-visible frames on $target_date — skipping video build"
|
|
exit 0
|
|
fi
|
|
|
|
echo "encoding $count frames into $final ..."
|
|
ffmpeg -loglevel warning -y \
|
|
-framerate "$FPS" \
|
|
-i "$work_dir/%06d.jpg" \
|
|
-c:v libx264 -pix_fmt yuv420p -preset "${ENCODE_PRESET:-slow}" \
|
|
-crf "$CRF" \
|
|
"$final"
|
|
|
|
echo "moon-track: $final ($(du -h "$final" | cut -f1))"
|
|
|
|
# Retention sweep
|
|
RETENTION="${MOON_TRACK_RETENTION_DAYS:-90}"
|
|
if [ "$RETENTION" -gt 0 ]; then
|
|
find "$MOVIES_DIR/$CAM_NAME/moon-track" -type f -name '*-moon-track.mp4' \
|
|
-mtime +"$RETENTION" -delete 2>/dev/null || true
|
|
fi
|