#!/bin/bash # ============================================================================= # CALIBRATION CRF — HEVC # # Encode un même extrait de rush à plusieurs CRF et mesure la qualité # objective (VMAF si dispo, sinon SSIM) + la taille obtenue. # Objectif : choisir CRF_HD / CRF_SD sur TES images plutôt qu'au doigt mouillé. # # Usage : # ./bench_crf.sh rushs/GH011182.MP4 [start_sec] [duree_sec] # # Variables : # CODEC=hevc palier 1080p HEVC (défaut) # CODEC=h264 palier 720p H.264 # HEIGHT=1080 hauteur cible du test (1080 ou 720) # CRFS="22 24 26 28 30 32" # PRESET=medium # ENCODER=cpu|vt (en mode vt, CRFS est interprété comme des valeurs -q:v) # KEEP=1 conserve les fichiers encodés pour comparaison visuelle # # Les deux paliers du site se calibrent séparément — les échelles de CRF # de x264 et x265 ne sont PAS comparables : # ./bench_crf.sh rushs/GH011182.MP4 30 20 # 1080p HEVC # CODEC=h264 HEIGHT=720 CRFS="20 22 24 26" ./bench_crf.sh rushs/GH011182.MP4 30 20 # # Choisis un extrait REPRÉSENTATIF : du mouvement, de la végétation, du # ciel dégradé. Un plan fixe sur un mur donnera un CRF trop optimiste. # ============================================================================= set -uo pipefail SRC="${1:?Usage: ./bench_crf.sh [start_sec] [duree_sec]}" START="${2:-30}" DUR="${3:-20}" CODEC="${CODEC:-hevc}" HEIGHT="${HEIGHT:-1080}" if [ "$CODEC" = "h264" ]; then CRFS="${CRFS:-18 20 22 24 26 28}" else CRFS="${CRFS:-22 24 26 28 30 32}" fi PRESET="${PRESET:-medium}" ENCODER="${ENCODER:-cpu}" KEEP="${KEEP:-0}" [ -f "$SRC" ] || { echo "❌ Fichier introuvable : $SRC"; exit 1; } WORK="$(mktemp -d "${TMPDIR:-/tmp}/bench_crf.XXXXXX")" cleanup() { [ "$KEEP" = "1" ] || rm -rf "$WORK"; } trap cleanup EXIT # --- Détection de la métrique disponible ------------------------------------- FILTERS_LIST="$(ffmpeg -hide_banner -filters 2>/dev/null)" if printf '%s' "$FILTERS_LIST" | grep -q "libvmaf"; then METRIC="vmaf" else METRIC="ssim" echo "ℹ️ libvmaf absent de ton ffmpeg → repli sur SSIM." echo " (VMAF est bien plus parlant : brew install ffmpeg l'inclut normalement)" fi # --- Référence : extrait copié sans réencodage ------------------------------- REF="$WORK/ref.mp4" echo "✂️ Extraction de la référence (${DUR}s à partir de ${START}s)…" ffmpeg -nostdin -hide_banner -loglevel error -y \ -ss "$START" -i "$SRC" -t "$DUR" \ -map 0:v:0 -c copy -tag:v hvc1 "$REF" 2>/dev/null if [ ! -s "$REF" ]; then echo "⚠️ Copie directe impossible, réencodage sans perte visible de la référence…" ffmpeg -nostdin -hide_banner -loglevel error -y \ -ss "$START" -i "$SRC" -t "$DUR" \ -map 0:v:0 -c:v libx265 -preset ultrafast -x265-params lossless=1 \ -tag:v hvc1 "$REF" fi # Si on teste une hauteur inférieure à la source, la référence de comparaison # doit être à la MÊME définition que le test : on fabrique une référence scalée. SRC_H=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=p=0 "$REF" | head -1) REF_CMP="$REF" if [ "$SRC_H" != "$HEIGHT" ]; then REF_CMP="$WORK/ref_${HEIGHT}.mp4" echo "📐 Référence de comparaison rescalée en ${HEIGHT}p…" ffmpeg -nostdin -hide_banner -loglevel error -y -i "$REF" \ -vf "scale=-2:${HEIGHT}:flags=lanczos" \ -c:v libx265 -preset ultrafast -x265-params lossless=1 -tag:v hvc1 "$REF_CMP" fi REF_DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$REF" | cut -d. -f1) REF_DUR=${REF_DUR:-$DUR} echo echo "🎯 Palier testé : ${HEIGHT}p en $(echo "$CODEC" | tr a-z A-Z)" printf "%-6s %10s %12s %10s %10s\n" "CRF" "Taille" "Débit" "$(echo "$METRIC" | tr a-z A-Z)" "Encodage" printf "%s\n" "--------------------------------------------------------" BEST_LINE="" for crf in $CRFS; do OUT="$WORK/test_crf${crf}.mp4" if [ "$CODEC" = "h264" ]; then if [ "$ENCODER" = "vt" ]; then VOPTS=(-c:v h264_videotoolbox -profile:v high -q:v "$crf" -allow_sw 1 -realtime 0) else VOPTS=(-c:v libx264 -preset "$PRESET" -profile:v high -level 4.0 -crf "$crf" -g 120 -keyint_min 24) fi else if [ "$ENCODER" = "vt" ]; then VOPTS=(-c:v hevc_videotoolbox -profile:v main -q:v "$crf" -allow_sw 1 -realtime 0 -tag:v hvc1) else VOPTS=(-c:v libx265 -preset "$PRESET" -profile:v main -crf "$crf" -x265-params "keyint=120:min-keyint=24:aq-mode=3" -tag:v hvc1) fi fi T0=$(date +%s) ffmpeg -nostdin -hide_banner -loglevel error -y -i "$REF" \ -map 0:v:0 "${VOPTS[@]}" \ -vf "scale=-2:${HEIGHT}:flags=lanczos" \ -pix_fmt yuv420p \ -color_primaries bt709 -color_trc bt709 -colorspace bt709 \ -an "$OUT" 2>/dev/null T1=$(date +%s) ENC_TIME=$((T1 - T0)) [ -s "$OUT" ] || { printf "%-6s %10s\n" "$crf" "ÉCHEC"; continue; } BYTES=$(stat -f%z "$OUT" 2>/dev/null || stat -c%s "$OUT") MB=$(awk -v b="$BYTES" 'BEGIN{printf "%.1f", b/1048576}') KBPS=$(awk -v b="$BYTES" -v d="$REF_DUR" 'BEGIN{printf "%.0f", b*8/1000/d}') # --- Mesure de qualité --- if [ "$METRIC" = "vmaf" ]; then SCORE=$(ffmpeg -nostdin -hide_banner -loglevel info -i "$OUT" -i "$REF_CMP" \ -lavfi "[0:v]setpts=PTS-STARTPTS[d];[1:v]setpts=PTS-STARTPTS[r];[d][r]libvmaf" \ -f null - 2>&1 | grep -oE "VMAF score: [0-9.]+" | tail -1 | grep -oE "[0-9.]+") SCORE=$(awk -v s="${SCORE:-0}" 'BEGIN{printf "%.2f", s}') else SCORE=$(ffmpeg -nostdin -hide_banner -loglevel info -i "$OUT" -i "$REF_CMP" \ -lavfi "[0:v]setpts=PTS-STARTPTS[d];[1:v]setpts=PTS-STARTPTS[r];[d][r]ssim" \ -f null - 2>&1 | grep -oE "All:[0-9.]+" | tail -1 | cut -d: -f2) SCORE=$(awk -v s="${SCORE:-0}" 'BEGIN{printf "%.4f", s}') fi printf "%-6s %9s M %9s k %10s %8s s\n" "$crf" "$MB" "$KBPS" "$SCORE" "$ENC_TIME" done echo echo "📖 Comment lire ce tableau" if [ "$METRIC" = "vmaf" ]; then cat <<'EOF' VMAF ≥ 95 : indiscernable de la source à distance normale VMAF 90-95 : excellent, cible raisonnable pour un site de voyage VMAF 85-90 : bon, quelques artefacts visibles en plein écran VMAF < 85 : dégradation nette sur les scènes chargées → Prends le CRF le plus ÉLEVÉ qui reste au-dessus de ton seuil. Le gain de taille entre deux CRF consécutifs s'écrase vite ; dès que la courbe de qualité plonge, tu es allé trop loin. EOF else cat <<'EOF' SSIM ≥ 0.98 : très bon SSIM 0.96-0.98 : correct pour du web SSIM < 0.95 : dégradation visible → Prends le CRF le plus ÉLEVÉ qui reste au-dessus de ton seuil. ⚠️ SSIM est moins fiable que VMAF sur des images très texturées (feuillage, eau, gravier) : vérifie AUSSI à l'œil avec KEEP=1. EOF fi if [ "$KEEP" = "1" ]; then echo echo "📁 Fichiers conservés dans : $WORK" echo " Compare à l'œil, en plein écran, sur les plans les plus chargés." fi