#!/usr/bin/env python3 """Compare two QA captures pixel by pixel. Usage: compare.py vanilla.png gpu.png OUT_PREFIX [--threshold PCT] [--delta N] Counts pixels whose max channel delta exceeds --delta (default 16, the value the development parity harness settled on: below that it is antialiasing fringe, not structure). PASS if the percentage of such pixels is below --threshold (default 0.5). Writes OUT_PREFIX-triptych.png (vanilla | gpu | diff heatmap) and prints one JSON line with the verdict. """ import json import sys from PIL import Image, ImageChops, ImageFilter def main(): args = [a for a in sys.argv[1:] if not a.startswith("--")] opts = {a.split("=")[0].lstrip("-"): float(a.split("=")[1]) for a in sys.argv[1:] if a.startswith("--")} vanilla_path, gpu_path, prefix = args threshold = opts.get("threshold", 0.5) delta = int(opts.get("delta", 16)) a = Image.open(vanilla_path).convert("RGB") b = Image.open(gpu_path).convert("RGB") if a.size != b.size: print(json.dumps({"pass": False, "error": "size mismatch", "vanilla": a.size, "gpu": b.size})) return 1 diff = ImageChops.difference(a, b) # max channel delta per pixel, then threshold gray = diff.convert("L") mask = gray.point(lambda p: 255 if p > delta else 0) hist = mask.histogram() bad = hist[255] total = a.size[0] * a.size[1] pct = 100.0 * bad / total # Anti-aliasing fringe is a 1px halo on glyph edges and varies between # rasterizers; real defects (wrong color, missing decoration, shifted # block) are contiguous blobs. A 3x3 erosion kills the halo and keeps # the blobs, so the PASS gate runs on the eroded mask. blob_mask = mask.filter(ImageFilter.MinFilter(3)) blob_bad = blob_mask.histogram()[255] blob_pct = 100.0 * blob_bad / total # triptych: vanilla | gpu | heatmap (red = blobs that gate the verdict, # dark red = AA fringe that does not) heat = a.point(lambda p: p // 3) fringe = Image.new("RGB", a.size, (110, 24, 24)) heat.paste(fringe, mask=mask) red = Image.new("RGB", a.size, (255, 32, 32)) heat.paste(red, mask=blob_mask) strip = Image.new("RGB", (a.size[0] * 3 + 20, a.size[1]), (24, 24, 24)) strip.paste(a, (0, 0)) strip.paste(b, (a.size[0] + 10, 0)) strip.paste(heat, (a.size[0] * 2 + 20, 0)) strip.save(prefix + "-triptych.png") ok = blob_pct <= threshold print(json.dumps({"pass": ok, "blob_pct": round(blob_pct, 4), "fringe_pct": round(pct, 4), "blob_pixels": blob_bad, "total": total, "threshold_pct": threshold, "delta": delta})) return 0 if ok else 1 if __name__ == "__main__": sys.exit(main())