63 lines
2.7 KiB
Python
Executable File
63 lines
2.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Where does each config family meet a VMAF target? Reads sweep.sh TSVs.
|
|
|
|
tools/encode/ladder.py results/x265-ladder.tsv [--target vmaf:95 --target p1:90 ...]
|
|
|
|
A family is a config name minus its trailing -CRF (x265-rect-16 -> x265-rect;
|
|
x265-crf16 -> x265).
|
|
For each family and clip, log(Mb/s) and CRF are interpolated linearly in the
|
|
chosen metric between the two ladder rungs that bracket the target. A tier
|
|
uses one CRF for everything, so the clip needing the lowest CRF binds it; that
|
|
CRF is printed last as "all". A dash means the ladder doesn't reach the target.
|
|
"""
|
|
import argparse
|
|
import csv
|
|
import math
|
|
import re
|
|
from collections import defaultdict
|
|
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("tsv", nargs="+")
|
|
ap.add_argument("--target", action="append",
|
|
help="metric:value, metric one of vmaf, p1, min, neg (default vmaf:95, p1:90, p1:97)")
|
|
args = ap.parse_args()
|
|
targets = [(m, float(v)) for m, v in (t.split(":") for t in (args.target or ["vmaf:95", "p1:90", "p1:97"]))]
|
|
col = {"vmaf": "vmaf", "p1": "vmaf_p1", "min": "vmaf_min", "neg": "vmaf_neg"}
|
|
|
|
fam = defaultdict(lambda: defaultdict(list)) # family -> clip -> [(crf, row)]
|
|
for path in args.tsv:
|
|
for r in csv.DictReader(open(path), delimiter="\t"):
|
|
m = re.fullmatch(r"(.*)-(?:crf)?(\d+)", r["config"])
|
|
if m:
|
|
fam[m[1]][r["clip"]].append((int(m[2]), r))
|
|
|
|
|
|
def solve(pts, key, target):
|
|
"""(crf, mbps, fps) where metric `key` crosses `target`, or None."""
|
|
pts = sorted(pts, key=lambda p: p[0]) # quality falls as CRF rises
|
|
for (c0, a), (c1, b) in zip(pts, pts[1:]):
|
|
q0, q1 = float(a[key]), float(b[key])
|
|
if q0 >= target >= q1 and q0 != q1:
|
|
t = (q0 - target) / (q0 - q1)
|
|
lr = math.log(float(a["mbps"])) * (1 - t) + math.log(float(b["mbps"])) * t
|
|
fps = float(a["fps"]) * (1 - t) + float(b["fps"]) * t
|
|
return c0 + t * (c1 - c0), math.exp(lr), fps
|
|
return None
|
|
|
|
|
|
for metric, target in targets:
|
|
print(f"\n## {metric} >= {target:g} (CRF @ Mb/s, fps)")
|
|
clips = sorted({c for f in fam.values() for c in f})
|
|
print(f"{'family':24}" + "".join(f"{c:>22}" for c in clips) + f"{'all (binding clip)':>24}")
|
|
for f, per in fam.items():
|
|
cells, worst = [], None
|
|
for c in clips:
|
|
s = solve(per.get(c, []), col[metric], target)
|
|
cells.append(f"{s[0]:5.1f} @ {s[1]:6.1f}, {s[2]:4.1f}" if s else "-")
|
|
if s is None:
|
|
worst = "missing"
|
|
elif worst != "missing" and (worst is None or s[0] < worst[0]):
|
|
worst = (s[0], c)
|
|
w = "-" if worst in (None, "missing") else f"CRF {worst[0]:4.1f} ({worst[1]})"
|
|
print(f"{f:24}" + "".join(f"{x:>22}" for x in cells) + f"{w:>24}")
|