4-seasons: validate JPEGs with ffprobe, add optional grey-frame filter

The previous size check caught empty files but not corrupt JPEGs with
a partial header (common first frame after a camera reconnect).
Replace with ffprobe codec detection — if ffprobe can't identify a
video stream in the file it's skipped with a clear "corrupt" message.

Also adds SEASONS_GREY_STDDEV_MIN (default 0 = off): when enabled,
frames with near-zero pixel standard deviation (uniform grey = bad
RTSP signal) are silently dropped before encoding.

https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
This commit is contained in:
Claude
2026-04-22 16:07:29 +00:00
parent 292824b48d
commit 4aeee6b3d5
2 changed files with 37 additions and 8 deletions
+27 -8
View File
@@ -87,17 +87,36 @@ cleanup() { rm -f "$temp_file" "$temp_video" 2>/dev/null || true; }
trap cleanup EXIT
mapfile -t all_images < <(find "$image_dir" -type f -name "*.jpg" | sort)
images=()
bad=0
# Validate each frame: skip empty files, corrupt JPEGs, and optionally grey/uniform frames.
# SEASONS_GREY_STDDEV_MIN=0 (default) disables the grey filter.
# Set to e.g. 5 in sky-cam.conf to drop uniform grey frames (bad-signal captures).
GREY_MIN="${SEASONS_GREY_STDDEV_MIN:-0}"
images=(); bad=0; grey=0
for img in "${all_images[@]}"; do
if [ -r "$img" ] && [ -s "$img" ]; then
images+=("$img")
else
echo "WARNING: skipping unreadable/empty frame: $img"
((bad++)) || true
# Empty or unreadable
if [ ! -r "$img" ] || [ ! -s "$img" ]; then
echo "WARNING: skipping empty/missing frame: $(basename "$img")"
((bad++)) || true; continue
fi
# Corrupt JPEG — ffprobe can't identify any video stream
if ! ffprobe -v quiet -select_streams v:0 \
-show_entries stream=codec_name -of csv=p=0 "$img" 2>/dev/null \
| grep -q .; then
echo "WARNING: skipping corrupt frame: $(basename "$img")"
((bad++)) || true; continue
fi
# Optional: skip uniform-grey bad-signal frames (low pixel standard deviation)
if [ "$GREY_MIN" != "0" ] && command -v convert &>/dev/null; then
stddev=$(convert "$img" -colorspace Gray -format "%[fx:int(standard_deviation*255)]" info: 2>/dev/null || echo "255")
if [ "${stddev:-255}" -lt "$GREY_MIN" ]; then
((grey++)) || true; continue
fi
fi
images+=("$img")
done
[ "$bad" -gt 0 ] && echo "WARNING: skipped $bad bad frame(s) out of ${#all_images[@]} found"
[ "$bad" -gt 0 ] && echo "WARNING: skipped $bad corrupt/empty frame(s) of ${#all_images[@]}"
[ "$grey" -gt 0 ] && echo "INFO: skipped $grey grey/uniform frame(s) (SEASONS_GREY_STDDEV_MIN=${GREY_MIN})"
for img in "${images[@]}"; do
printf "file '%s'\nduration %s\n" "$img" "$INTERVAL"
+10
View File
@@ -255,6 +255,16 @@ CRF_MONTAGE_FINAL=22 # montage-mvt.sh — final archive montage (H
ENCODE_PRESET=slow # ffmpeg preset for all encodes (slow/medium/fast)
AUDIO_BITRATE=192k # montage-mvt.sh — music track on movement montages
# ── Frame validation (4-seasons.sh) ──────────────────────────────────────────
# Corrupt or empty frames (e.g. first frame after a camera reconnect) are always
# skipped automatically via ffprobe.
#
# SEASONS_GREY_STDDEV_MIN: also skip uniform-grey frames caused by a bad RTSP
# signal. These are valid JPEGs but contain a near-solid grey image.
# Value is the minimum pixel standard deviation on a 0255 scale.
# 0 = disabled (default). Try 510 if your camera produces grey frames on reconnect.
SEASONS_GREY_STDDEV_MIN=0
# ── Montage attribution overlay ───────────────────────────────────────────────
# A translucent bar across the top of each movement montage, naming the
# music source ("The Four Seasons — Antonio Vivaldi ...").