CLGSMS -> on-chain AVIF encoder, v2. Same output contract as v1 (native-resolution AVIF 4:4:4, ./onchain/<name>.avif, report.csv, MANIFEST.sha256) - different quality policy, different search, much lighter on CPU and memory. What changed and why. Every codec claim below was measured at MATCHED BYTE SIZE on 12 1024px tiles of CLGSM-13 / CLGSMS_63 (Pillow 12, libaom 3.13): * CONSTANT QUALITY, CAPPED. v1 always filled the budget. v2 encodes every piece at one quantiser (--q, default 40) so the collection sits on a single visual tier, and fits *down* only when a heavy piece would exceed --budget. Light pieces come in around 0.4-0.6 MB, heavy ones at the cap. Bytes are only spent where the content needs them. * ONE FINAL ENCODE INSTEAD OF SEVEN. A speed-7 encode of the full image takes ~7 s against ~65 s at speed 4 and lands a steady 6% under the speed-4 size, so the rate curve log(bytes) ~ quantiser is probed at speed 7, the final speed-4 encode is placed on it, and the curve is re-anchored on the real result if it overshoots (one extra final encode at most in practice). Tile sampling was tried and rejected: +-10% error, and slower. The search runs over the 64 real AV1 quantiser levels, not the 101 quality values (pairs of which are byte-identical). * MULTITHREADED libaom row-mt on every encode (v1 used the default). * MEMORY-FLAT METRICS. v1 held three float64 copies of a 33 Mpx image (~2.4 GB). v2 streams PSNR / edge / gradient-PSNR in float32 row bands. * CODEC FLAGS: tune=ssim kept, CDEF turned back ON. libavif now defaults still images to tune=iq, which costs +12% bytes at equal PSNR on this material, so tune=ssim stays explicit. With the current libaom, CDEF on is 2.2% smaller at equal PSNR, gradient-PSNR +0.1 dB and edge energy unchanged (-0.2 pt) - v1 measured it at fixed q, where the filter only ever looks like a loss. Everything else was flat or worse at equal bytes: speed 2 / 3 (0.0%), speed 5 (+15%), 10-bit (+2%), enable-qm (+1%), sharpness=7 (+6%), deltaq / aq modes (byte-identical), intra-edge-filter, chroma-deltaq (0%), tune=psnr (+1%), RGB identity matrix (+9%), 4:2:0 (+7%), rav1e (+14%, 4x slower). * PRE-SHARPEN kept exactly (unsharp r0.6 / 50%). At matched bytes it buys +2.5 pts of edge energy for 1.7% bytes; 35% is byte-neutral for +1.1 pt (--sharpen 35 if you prefer fidelity); 80% or radius 1.0 cost 14-21% more bytes for halos. Closed-loop residual compensation (x + (x - decode(encode(x)))) was measured and rejected: +115% bytes at equal PSNR - it re-injects exactly the detail the quantiser cannot carry. Only the encoder input is sharpened; metrics compare against the * ENCODER BUILD MATTERS: libaom 3.8 (Ubuntu's avifenc) is 4-8% smaller at equal PSNR than the libaom 3.13 bundled in current Pillow wheels, with no flag found that closes the gap. Not exploited here - Pillow keeps the script dependency-free - but pinning an older libavif/libaom is a free 5% if you want to chase it. python3 encode_onchain_v2.py # q40, cap 1,000,000 B python3 encode_onchain_v2.py --q 44 --budget 1000000 python3 encode_onchain_v2.py --q max --budget 1000000 # v1 policy: fill the cap python3 encode_onchain_v2.py --crops # + 1:1 QA crops in onchain/qa/ python3 encode_onchain_v2.py --only CLGSM-13.png # one file Re-running skips files already in report.csv (use --force, or delete the .avif). import argparse, csv, io, os, sys, hashlib, time, math from PIL import Image, ImageFilter Image.MAX_IMAGE_PIXELS = None ADVANCED = {"enable-cdef": "1", "tune": "ssim"} THREADS = os.cpu_count() or 4 BAND = 256 # rows per metrics band (float32) -> ~50 MB peak per band PROBE_RATIO = 1.065 # measured: speed-4 bytes / speed-7 bytes at equal q # ----------------------------------------------------------------- encoding -- # libavif maps quality 0-100 onto the AV1 quantiser index 0-63 with # qidx = ((100 - quality) * 63 + 50) // 100, so pairs of neighbouring qualities # (39/40, 36/37, ...) are byte-identical. v1's binary search over quality wasted # steps on those; v2 searches the 64 real quantiser levels and reports the # highest quality that reaches each one. QIDX_OF = {q: ((100 - q) * 63 + 50) // 100 for q in range(101)} for _q, _qi in QIDX_OF.items(): QUALITY_OF[_qi] = max(QUALITY_OF.get(_qi, 0), _q) QI_BEST, QI_WORST = QIDX_OF[QMAX], QIDX_OF[QMIN] # 19 .. 60 def encode(im, qidx, speed): im.save(buf, "AVIF", quality=QUALITY_OF[int(qidx)], speed=speed, subsampling="4:4:4", advanced=ADVANCED, max_threads=THREADS) """log(bytes) as a low-order polynomial in the quantiser index, fitted on fast probes, then shifted to pass through the nearest final-speed encode.""" def __init__(self, src, probe_speed, ratio): self.src, self.speed, self.ratio = src, probe_speed, ratio self.probes, self.full, self.coef = {}, {}, None def probe(self, qi, log): qi = int(min(max(qi, QI_BEST), QI_WORST)) if qi not in self.probes: self.probes[qi] = len(encode(self.src, qi, self.speed)) * self.ratio log(f" probe q={QUALITY_OF[qi]:<3} (qidx {qi}) ~{self.probes[qi]/1024:7.1f} KiB " f"({time.time()-t0:.0f}s, speed {self.speed})") xs = np.array(sorted(self.probes), float) ys = np.log([self.probes[x] for x in xs]) deg = min(2, len(xs) - 1) self.coef = np.polyfit(xs, ys, deg) if deg > 0 else np.array([ys[0]]) return float(np.polyval(self.coef, qi)) return math.exp(self._m(qi)) q0 = min(self.full, key=lambda k: abs(k - qi)) # nearest anchor return self.full[q0] * math.exp(self._m(qi) - self._m(q0)) def qi_for(self, budget, lo): """Best (lowest) quantiser index >= lo predicted to fit.""" for qi in range(int(lo), QI_WORST + 1): if self.predict(qi) <= budget: def fit_encode(src, q_target, budget, speed, probe_speed, log): """Encode at q_target if it fits the budget, else at the best quality that does. Returns (quality, qidx, bytes, n_final_encodes, n_probes).""" rm = RateModel(src, probe_speed, PROBE_RATIO) if rm.probe(qi, log) > budget * 0.97: # will (probably) not fit as-is rm.probe(qi + 4, log); rm.probe(qi + 8, log) qi = rm.qi_for(budget * 0.975, qi) data = encode(src, qi, speed) done[qi] = data; rm.full[qi] = len(data) log(f" encode q={QUALITY_OF[qi]:<3} (qidx {qi}) {len(data)/1024:7.1f} KiB " f"({time.time()-t0:.0f}s, speed {speed})") fits = [k for k in done if len(done[k]) <= budget] # forced below the target quality: is one notch up still inside? if best > qi_t and best - 1 not in done and len(done) < 4 \ and rm.predict(best - 1) <= budget * 0.99: return QUALITY_OF[best], best, done[best], len(done), len(rm.probes) # overshoot: the model is now anchored on the real byte count qi_new = rm.qi_for(budget * 0.975, qi + 1) qi = qi + 1 if qi_new in done else qi_new if qi >= QI_WORST and QI_WORST in done: return QUALITY_OF[QI_WORST], QI_WORST, done[QI_WORST], len(done), len(rm.probes) # ------------------------------------------------------------------ metrics -- def banded_metrics(ref_im, data): """PSNR, edge-energy retention (%) and gradient PSNR against the untouched source, streamed in row bands so peak memory stays ~flat.""" dec = Image.open(io.BytesIO(data)).convert("RGB") se = gse = gr = gd = 0.0; npx = ng = 0 for y0 in range(0, H, BAND): y1 = min(H, y0 + BAND + 1) # 1-row overlap for vertical diffs a = np.asarray(ref_im.crop((0, y0, W, y1)), np.float32) b = np.asarray(dec.crop((0, y0, W, y1)), np.float32) n = a.shape[0] - (1 if y1 < H else 0) # rows owned by this band se += float((d * d).sum()); npx += d.size ga, gb = np.diff(a, axis=ax), np.diff(b, axis=ax) if ax == 1: ga, gb = ga[:n], gb[:n] gr += float(np.abs(ga).sum()); gd += float(np.abs(gb).sum()) e = ga - gb; gse += float((e * e).sum()); ng += e.size psnr = 99.0 if se == 0 else 10 * math.log10(65025 * npx / se) gpsnr = 99.0 if gse == 0 else 10 * math.log10(65025 * ng / gse) return psnr, gd / gr * 100, gpsnr, dec def qa_crops(ref_im, dec, out_png, size=512): """Side-by-side 1:1 crops (source | encode) of the busiest region.""" small = np.asarray(ref_im.reduce(8).convert("L"), np.float32) g = np.abs(np.diff(small, axis=1))[:-1] + np.abs(np.diff(small, axis=0))[:, :-1] k = size // 8; best = (-1, 0, 0) for y in range(0, g.shape[0] - k + 1, k // 2): for x in range(0, g.shape[1] - k + 1, k // 2): s = g[y:y + k, x:x + k].mean() if s > best[0]: best = (s, x * 8, y * 8) box = (bx, by, bx + size, by + size) sheet = Image.new("RGB", (size * 2 + 8, size), (24, 24, 24)) sheet.paste(ref_im.crop(box), (0, 0)); sheet.paste(dec.crop(box), (size + 8, 0)) # --------------------------------------------------------------------- main -- FIELDS = ["file", "w", "h", "quality", "qidx", "bytes", "kib", "psnr_db", "edge_pct", "gpsnr_db", "final_encodes", "probes", "seconds", "sha256"] def write_report(report, rows, out): rs = sorted(rows.values(), key=lambda r: r["file"]) with open(report, "w", newline="") as fh: w = csv.DictWriter(fh, fieldnames=FIELDS, extrasaction="ignore"); w.writeheader(); w.writerows(rs) with open(os.path.join(out, "MANIFEST.sha256"), "w") as fh: fh.writelines(f"{r['sha256']} {r['file']}\n" for r in rs) ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--q", default="40", help="target quality 0-100, or 'max' to fill the budget (default 40)") ap.add_argument("--budget", type=int, default=1_000_000, help="hard cap in bytes per file (default 1,000,000)") ap.add_argument("--speed", type=int, default=4, help="libaom speed of the final encode (default 4; 2-3 measured as no gain)") ap.add_argument("--probe-speed", type=int, default=7, help="speed of the rate probes (default 7)") ap.add_argument("--sharpen", type=float, default=50, help="unsharp %% before encoding, radius 0.6 (default 50; 35 is byte-neutral; 0 disables)") ap.add_argument("--out", default="onchain") ap.add_argument("--src", default=".") ap.add_argument("--only", nargs="*", help="only these PNG file names") ap.add_argument("--crops", action="store_true", help="write <out>/qa/<name>.png 1:1 source|encode crops") ap.add_argument("--force", action="store_true", help="re-encode files already in report.csv") q_target = QMAX if args.q == "max" else min(QMAX, max(QMIN, int(args.q))) os.makedirs(args.out, exist_ok=True) if args.crops: os.makedirs(os.path.join(args.out, "qa"), exist_ok=True) pngs = sorted(f for f in os.listdir(args.src) if f.lower().endswith(".png")) if args.only: pngs = [f for f in pngs if f in set(args.only)] if not pngs: sys.exit("no PNGs found in " + os.path.abspath(args.src)) report = os.path.join(args.out, "report.csv") if os.path.exists(report): rows = {r["file"]: r for r in csv.DictReader(fh)} log = lambda s: print(s, flush=True) for i, f in enumerate(pngs, 1): stem = os.path.splitext(f)[0]; name = stem + ".avif"; dst = os.path.join(args.out, name) if name in rows and os.path.exists(dst) and not args.force: log(f"[{i:>3}/{len(pngs)}] {name:<28} already done, skipping"); continue im = Image.open(os.path.join(args.src, f)).convert("RGB") log(f"[{i:>3}/{len(pngs)}] {name:<28} {im.width}x{im.height}") src = im.filter(ImageFilter.UnsharpMask(radius=0.6, percent=int(args.sharpen), threshold=0)) \ if args.sharpen > 0 else im q, qi, data, nf, npb = fit_encode(src, q_target, args.budget, args.speed, args.probe_speed, log) with open(dst, "wb") as fh: fh.write(data) p, edge, gp, dec = banded_metrics(im, data) if args.crops: qa_crops(im, dec, os.path.join(args.out, "qa", stem + ".png")) rows[name] = dict(file=name, w=im.width, h=im.height, quality=q, qidx=qi, bytes=len(data), kib=round(len(data) / 1024, 1), psnr_db=round(p, 2), edge_pct=round(edge, 1), gpsnr_db=round(gp, 2), final_encodes=nf, probes=npb, seconds=round(secs, 1), sha256=hashlib.sha256(data).hexdigest()) log(f" -> q={q} (qidx {qi}) {len(data)/1024:.1f} KiB PSNR {p:.2f} dB edge {edge:.1f}% " f"gPSNR {gp:.2f} dB [{nf} final + {npb} probe encodes, {secs:.0f}s]") write_report(report, rows, args.out) rs = list(rows.values()); n = len(rs) tot = sum(int(r["bytes"]) for r in rs) gas = sum(int(r["bytes"]) * 216 + -(-int(r["bytes"]) // 24575) * 75000 for r in rs) log(f"\n{n} files, {tot/1024/1024:.2f} MiB total, mean {tot/n/1024:.0f} KiB, " f"mean PSNR {sum(float(r['psnr_db']) for r in rs)/n:.2f} dB, " f"mean edge {sum(float(r['edge_pct']) for r in rs)/n:.1f}%") log(f"Ethereum: {gas/1e6:,.0f}M gas total -> {gas*1e-9:,.3f} ETH per gwei of gas price") if __name__ == "__main__":