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:
@@ -0,0 +1,3 @@
|
|||||||
|
clips/
|
||||||
|
work/
|
||||||
|
results/
|
||||||
@@ -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
|
||||||
Executable
+66
@@ -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
|
||||||
Executable
+87
@@ -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"
|
||||||
Reference in New Issue
Block a user