feat: add encoder tuning harness for 4K60 fractal videos

make_clips.sh renders lossless FFV1 4K60 test clips through the Makefile's
colour conversion; sweep.sh encodes them with a list of ffmpeg configs and
scores each with VMAF 4K (mean/p1/min), VMAF-NEG, CAMBI banding and PSNR-Y.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 17:59:14 +02:00
co-authored by Claude Opus 5.5
parent 49130f5ecf
commit da992c139a
4 changed files with 164 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
clips/
work/
results/
+8
View File
@@ -0,0 +1,8 @@
# Pass 1: which presets fit the 3-5 fps budget at 4K on this machine.
x265-fast | -pix_fmt yuv420p10le -c:v libx265 -profile:v main10 -preset fast -crf 18 -x265-params log-level=error:aq-mode=3:no-sao=1
x265-medium | -pix_fmt yuv420p10le -c:v libx265 -profile:v main10 -preset medium -crf 18 -x265-params log-level=error:aq-mode=3:no-sao=1
x265-slow | -pix_fmt yuv420p10le -c:v libx265 -profile:v main10 -preset slow -crf 18 -x265-params log-level=error:aq-mode=3:no-sao=1
av1-p5 | -pix_fmt yuv420p10le -c:v libsvtav1 -preset 5 -crf 26 -svtav1-params film-grain=0
av1-p6 | -pix_fmt yuv420p10le -c:v libsvtav1 -preset 6 -crf 26 -svtav1-params film-grain=0
av1-p7 | -pix_fmt yuv420p10le -c:v libsvtav1 -preset 7 -crf 26 -svtav1-params film-grain=0
av1-p8 | -pix_fmt yuv420p10le -c:v libsvtav1 -preset 8 -crf 26 -svtav1-params film-grain=0
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Render the lossless 4K60 test clips that sweep.sh encodes.
#
# Each clip goes through the same RGBA -> bt709 tv-range 10-bit conversion as
# the Makefile, then into lossless FFV1. The clips are therefore exactly what the
# encoders see, so VMAF against them measures the encoder alone.
#
# tools/encode/make_clips.sh [--size WxH] [--seconds S] [--force] [clip...]
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
root=$(cd "$here/../.." && pwd)
out="$here/clips"
size=3840x2160
seconds=4
fps=60
force=0
only=()
while [[ $# -gt 0 ]]; do
case $1 in
--size) size=$2; shift 2 ;;
--seconds) seconds=$2; shift 2 ;;
--force) force=1; shift ;;
-h|--help) sed -n '2,9p' "$0"; exit 0 ;;
*) only+=("$1"); shift ;;
esac
done
# Seahorse valley, minibrot at 3.7e-41 (tools/deep-zoom/LOCATIONS.md).
SH_RE=-0.77568376800905379745347652613832924487504096622022
SH_IM=0.13646736829469012473375311880735014411233827361594
# name | mandelbrot flags. Zoom speeds bracket a typical 60 s dive (~0.7 decade/s).
clips=(
# Worst case: dense filaments, 1.5 decades/s of scaling motion.
"filaments|--view=$SH_RE,$SH_IM,1e-8 --to-view=$SH_RE,$SH_IM,3e-15 --linear"
# Smooth concentric bands closing in on the minibrot: banding/blocking risk.
"bands|--view=$SH_RE,$SH_IM,1e-38 --to-view=$SH_RE,$SH_IM,1.1e-40 --linear"
# Embedded Julia set, slow drift: fine static texture, psy/detail retention.
"julia|--view=$SH_RE,$SH_IM,2e-30 --to-view=$SH_RE,$SH_IM,1e-30 --linear"
)
[[ -x $root/target/release/mandelbrot ]] || cargo build --release --manifest-path "$root/Cargo.toml"
mkdir -p "$out"
w=${size%x*}
h=${size#*x}
for entry in "${clips[@]}"; do
name=${entry%%|*}
flags=${entry#*|}
if [[ ${#only[@]} -gt 0 && ! " ${only[*]} " =~ " $name " ]]; then continue; fi
dst="$out/$name.mkv"
if [[ -f $dst && $force = 0 ]]; then echo "skip $name (exists, --force to redo)"; continue; fi
echo "render $name ($size, ${seconds}s @ ${fps}fps)"
# shellcheck disable=SC2086
"$root/target/release/mandelbrot" --headless --antialias $flags \
--width "$w" --height "$h" --fps "$fps" --duration "$seconds" --export-path - |
ffmpeg -hide_banner -loglevel error -y \
-f rawvideo -pix_fmt rgba -s "$size" -framerate "$fps" -i - \
-vf "scale=out_color_matrix=bt709:out_range=tv:flags=accurate_rnd+full_chroma_int+bitexact,format=yuv420p10le" \
-c:v ffv1 -level 3 -slices 16 -g 1 \
-colorspace bt709 -color_primaries bt709 -color_trc bt709 -color_range tv \
"$dst.tmp.mkv"
mv "$dst.tmp.mkv" "$dst"
done
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# Encode every test clip with every config, then score each encode.
#
# tools/encode/sweep.sh [--clips a,b] [--keep] [--tag NAME] CONFIG_FILE...
#
# A config line is `name | ffmpeg output args` (blank lines and # comments are
# skipped). The args go between `-i clip.mkv` and the output file, e.g.
# x265-crf18 | -c:v libx265 -preset medium -crf 18 -x265-params aq-mode=3
# Each row of results/<tag>.tsv has encode fps, bitrate, VMAF 4K (mean, 1st
# percentile, min), VMAF-NEG mean, CAMBI banding (mean, max) and PSNR-Y.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
clipdir="$here/clips"
model=/usr/share/model
export SVT_LOG=1 # errors only
clips=""
keep=0
tag=$(date +%Y%m%d-%H%M%S)
configs=()
while [[ $# -gt 0 ]]; do
case $1 in
--clips) clips=$2; shift 2 ;;
--keep) keep=1; shift ;;
--tag) tag=$2; shift 2 ;;
-h|--help) sed -n '2,11p' "$0"; exit 0 ;;
*) configs+=("$1"); shift ;;
esac
done
[[ ${#configs[@]} -gt 0 ]] || { echo "usage: $0 [--clips a,b] [--keep] [--tag NAME] CONFIG_FILE..." >&2; exit 1; }
if [[ -z $clips ]]; then
clips=$(cd "$clipdir" && ls ./*.mkv | sed 's|^\./||; s|\.mkv$||' | paste -sd,)
fi
[[ -n $clips ]] || { echo "no clips in $clipdir: run make_clips.sh first" >&2; exit 1; }
mkdir -p "$here/results" "$here/work"
tsv="$here/results/$tag.tsv"
[[ -f $tsv ]] || printf 'clip\tconfig\tfps\tmbps\tvmaf\tvmaf_p1\tvmaf_min\tvmaf_neg\tcambi\tcambi_max\tpsnr_y\n' > "$tsv"
IFS=, read -ra clip_list <<< "$clips"
for cfg in "${configs[@]}"; do
while IFS= read -r line || [[ -n $line ]]; do
[[ $line =~ ^[[:space:]]*(#|$) ]] && continue
name=$(sed 's/[[:space:]]*|.*//' <<< "$line")
args=$(sed 's/^[^|]*|[[:space:]]*//' <<< "$line")
for clip in "${clip_list[@]}"; do
ref="$clipdir/$clip.mkv"
enc="$here/work/$clip--$name.mp4"
log="$here/work/$clip--$name.json"
frames=$(ffprobe -v error -count_packets -select_streams v:0 \
-show_entries stream=nb_read_packets -of csv=p=0 "$ref")
printf '%-10s %-40s ' "$clip" "$name"
t0=$(date +%s.%N)
# shellcheck disable=SC2086
ffmpeg -hide_banner -loglevel error -y -i "$ref" $args -an "$enc" < /dev/null
t1=$(date +%s.%N)
# 4K model for both VMAF variants; CAMBI flags banding, which VMAF
# barely sees and which is the main risk on smooth fractal gradients.
ffmpeg -hide_banner -loglevel error -i "$enc" -i "$ref" -lavfi \
"[0:v]setpts=PTS-STARTPTS[d];[1:v]setpts=PTS-STARTPTS[r];[d][r]libvmaf=log_fmt=json:log_path=$log:n_threads=$(nproc):n_subsample=2:model='path=$model/vmaf_4k_v0.6.1.json\\:name=vmaf|path=$model/vmaf_4k_v0.6.1neg.json\\:name=neg':feature='name=cambi|name=psnr'" \
-f null - < /dev/null
row=$(python3 - "$log" "$enc" "$frames" "$t0" "$t1" <<'PY'
import json, os, sys
log, enc, frames, t0, t1 = sys.argv[1], sys.argv[2], int(sys.argv[3]), float(sys.argv[4]), float(sys.argv[5])
fr = json.load(open(log))["frames"]
col = lambda k: sorted(f["metrics"][k] for f in fr)
v = col("vmaf")
p1 = v[max(0, int(0.01 * len(v)) - 1)] if len(v) >= 100 else v[0]
mean = lambda xs: sum(xs) / len(xs)
cambi = col("cambi")
fps = frames / (t1 - t0)
mbps = os.path.getsize(enc) * 8 / (frames / 60) / 1e6
print(f"{fps:.2f}\t{mbps:.1f}\t{mean(v):.2f}\t{p1:.2f}\t{v[0]:.2f}\t{mean(col('neg')):.2f}\t{mean(cambi):.2f}\t{cambi[-1]:.2f}\t{mean(col('psnr_y')):.2f}")
PY
)
printf '%s\t%s\t%s\n' "$clip" "$name" "$row" >> "$tsv"
awk -F'\t' '{printf "%6s fps %7s Mb/s vmaf %s p1 %s min %s neg %s cambi %s/%s psnr %s\n",$1,$2,$3,$4,$5,$6,$7,$8,$9}' <<< "$row"
[[ $keep = 1 ]] || rm -f "$enc"
done
done < "$cfg"
done
echo "results: $tsv"