Download eval_ocr.py from skyuu72/grinpress-v3: direct link, hf CLI and curl.
- Browser
- Download file 11.3 kB
-
https://huggingface.co/skyuu72/grinpress-v3/resolve/main/eval_ocr.py
- Command line
-
hf download hf://skyuu72/grinpress-v3/eval_ocr.py
-
curl -L -o eval_ocr.py https://huggingface.co/skyuu72/grinpress-v3/resolve/main/eval_ocr.py
11.3 kB
| """grinpress v3 OCR evaluation. | |
| python eval_ocr.py refs # Phase 2: tesseract v2 / tesseract v3 / CRNN on the 48 reference renders | |
| python eval_ocr.py models --unet onnx/unet_distilled_int8.onnx --text onnx/text_encoder_int8.onnx [--teacher ckpt] | |
| # Phase 5: 48-grid at 2 and 4 steps (+14-grid teacher vs student) -> assets/eval_ocr_v3.json, eval_compare_v3.json, | |
| # samples_4step_v3.png, samples_2step_v3.png, teacher_vs_student_v3.png | |
| Scores (per string): exact = case-sensitive exact match after whitespace normalisation; exact_ci = v2's metric | |
| (upper-cased, stray face-edge glyph stripped); char_acc = 1 - Levenshtein/len on the exact_ci-normalised strings | |
| (v2 definition), char_acc_cs = same on case-sensitive strings. Primary v3 metric: CRNN exact (case-sensitive). | |
| """ | |
| import argparse, json, os, random, re, sys, time | |
| import numpy as np, torch | |
| from PIL import Image, ImageDraw, ImageFont | |
| ROOT = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path.insert(0, ROOT) | |
| def lev(a, b): | |
| d = list(range(len(b) + 1)) | |
| for i, ca in enumerate(a, 1): | |
| p, d[0] = d[0], i | |
| for j, cb in enumerate(b, 1): | |
| p, d[j] = d[j], min(d[j] + 1, d[j - 1] + 1, p + (ca != cb)) | |
| return d[len(b)] | |
| def norm_ci(s): | |
| s = re.sub(r"\s+", " ", s.strip().upper()) | |
| return re.sub(r"^[)|}\].,'\u2018\u201c-] ", "", s) | |
| def norm_cs(s): | |
| return re.sub(r"\s+", " ", s.strip()) | |
| def score_text(pred, gt): | |
| p, g = norm_ci(pred), norm_ci(gt) | |
| pc, gc = norm_cs(pred), norm_cs(gt) | |
| return dict(exact=pc == gc, exact_ci=p == g, char_acc=max(0.0, 1 - lev(p, g) / max(1, len(g))), | |
| char_acc_cs=max(0.0, 1 - lev(pc, gc) / max(1, len(gc)))) | |
| def summarize(rows, key): | |
| P = [r[key] for r in rows] | |
| return dict(n=len(P), exact=sum(p["exact"] for p in P) / len(P), exact_ci=sum(p["exact_ci"] for p in P) / len(P), | |
| char_acc=float(np.mean([p["char_acc"] for p in P])), char_acc_cs=float(np.mean([p["char_acc_cs"] for p in P]))) | |
| def fonts(): | |
| from generate_data import ensure_fonts | |
| return sorted(ensure_fonts()) | |
| def reference_images(grid): | |
| from generate_data_v2 import render_v2 | |
| F = fonts() | |
| return [render_v2(128, random.Random(seed), F, **{k: sp[k] for k in ("color", "expr", "text", "layout", "paper")})[0] | |
| for sp, _, seed in grid] | |
| def ocr_all(imgs, layouts, crnn=None, tess=("v3",)): | |
| from crnn import recognize | |
| from ocr_tess import tess_v2, tess_v3 | |
| out = {} | |
| if crnn is not None: | |
| out["crnn"] = recognize(crnn, imgs, layouts) | |
| if "v2" in tess: | |
| out["tess_v2"] = [tess_v2(im, l) for im, l in zip(imgs, layouts)] | |
| if "v3" in tess: | |
| out["tess_v3"] = [tess_v3(im, l) for im, l in zip(imgs, layouts)] | |
| if "v3s" in tess: | |
| out["tess_v3_sauvola"] = [tess_v3(im, l, "sauvola") for im, l in zip(imgs, layouts)] | |
| return out | |
| FONT = ImageFont.load_default(size=13) | |
| def grid_png(imgs, labels, cols, path, cell=192, title=None): | |
| rows = (len(imgs) + cols - 1) // cols; lh = 34; th = 30 if title else 0 | |
| G = Image.new("RGB", (cols * cell, th + rows * (cell + lh)), "white"); d = ImageDraw.Draw(G) | |
| if title: d.text((6, 6), title, fill="black", font=ImageFont.load_default(size=16)) | |
| for k, (im, lab) in enumerate(zip(imgs, labels)): | |
| r, c = divmod(k, cols); x0, y0 = c * cell, th + r * (cell + lh) | |
| G.paste(im.resize((cell, cell), Image.NEAREST), (x0, y0)) | |
| for li, line in enumerate(lab.split("\n")[:2]): | |
| d.text((x0 + 3, y0 + cell + 2 + li * 15), line, fill="black", font=FONT) | |
| G.save(path); return path | |
| # ------------------------------------------------------------------ Phase 2: references | |
| def cmd_refs(a): | |
| from eval_grid import grid48 | |
| from crnn import load_crnn | |
| g = grid48(); imgs = reference_images(g); lays = [sp["layout"] for sp, _, _ in g]; texts = [sp["text"] for sp, _, _ in g] | |
| crnn = load_crnn(a.crnn) if a.crnn and os.path.exists(a.crnn) else None | |
| t0 = time.time() | |
| preds = ocr_all(imgs, lays, crnn, tess=("v2", "v3", "v3s")) | |
| res = {"what": "OCR on the 48 procedural reference renders (generate_data_v2.render_v2, seeds 1000+i, byte-identical to v2)", | |
| "crnn": a.crnn if crnn is not None else None, "per_sample": [], "summary": {}} | |
| for i, (sp, cap, seed) in enumerate(g): | |
| row = dict(text=sp["text"], layout=sp["layout"], paper=sp["paper"], expr=sp["expr"], seed=seed) | |
| for k, v in preds.items(): row[k] = dict(ocr=v[i], **score_text(v[i], sp["text"])) | |
| res["per_sample"].append(row) | |
| for k in preds: res["summary"][k] = summarize(res["per_sample"], k) | |
| res["seconds"] = round(time.time() - t0, 1) | |
| os.makedirs(os.path.dirname(a.out), exist_ok=True) | |
| json.dump(res, open(a.out, "w"), indent=1) | |
| print(json.dumps(res["summary"], indent=1)) | |
| for k in preds: | |
| miss = [(r["text"], r[k]["ocr"]) for r in res["per_sample"] if not r[k]["exact"]] | |
| print(k, "misses (case-sensitive):", miss[:60]) | |
| # ------------------------------------------------------------------ Phase 5: models | |
| def ort_sess(p, threads=os.cpu_count()): | |
| import onnxruntime as ort | |
| so = ort.SessionOptions(); so.intra_op_num_threads = threads; so.inter_op_num_threads = 1 | |
| so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL | |
| return ort.InferenceSession(p, so, providers=["CPUExecutionProvider"]) | |
| def noise(seed): | |
| return torch.randn(1, 3, 128, 128, generator=torch.Generator().manual_seed(seed)).numpy() | |
| def sched(n): | |
| return [1.0 - i / n for i in range(n + 1)] | |
| def student_onnx(te, un, caps, seeds, steps, bs=8): | |
| from conditioning import tokenize | |
| out = [] | |
| for b in range(0, len(caps), bs): | |
| c, sd = caps[b:b + bs], seeds[b:b + bs] | |
| ids, mk = tokenize(c) | |
| ctx, cm = te.run(None, {"input_ids": ids.numpy(), "attention_mask": mk.long().numpy()}) | |
| x = np.concatenate([noise(s) for s in sd]); ts = sched(steps) | |
| for i in range(steps): | |
| v = un.run(None, {"x_t": x, "t": np.full((len(c),), ts[i], np.float32), "context": ctx, "context_mask": cm})[0] | |
| x = x - (ts[i] - ts[i + 1]) * v | |
| out.append(np.clip(x, -1, 1)) | |
| return np.concatenate(out) | |
| def teacher_images(teacher_path, caps, seeds, steps=50, cfg=5.0, bs=8, device="cpu"): | |
| from train_teacher_v3 import load_flow, sample_cfg | |
| T = load_flow(teacher_path, device) | |
| out = [] | |
| for b in range(0, len(caps), bs): | |
| z = torch.from_numpy(np.concatenate([noise(s) for s in seeds[b:b + bs]])).to(device) | |
| out.append(sample_cfg(T, caps[b:b + bs], z, steps, cfg).cpu().numpy()) | |
| return np.concatenate(out) | |
| def to_img(x): | |
| return Image.fromarray(((x.transpose(1, 2, 0) + 1) * 127.5).round().clip(0, 255).astype(np.uint8)) | |
| def cmd_models(a): | |
| from eval_grid import grid48, grid14 | |
| from crnn import load_crnn | |
| crnn = load_crnn(a.crnn) | |
| tess = tuple(t for t in a.tess.split(",") if t) | |
| os.makedirs(a.assets, exist_ok=True) | |
| te, un = ort_sess(a.text), ort_sess(a.unet) | |
| g = grid48(); caps = [c for _, c, _ in g]; seeds = [s for *_, s in g]; lays = [sp["layout"] for sp, _, _ in g] | |
| imgs, res = {}, {"unet": os.path.basename(a.unet), "text_encoder": os.path.basename(a.text), "n": len(g), | |
| "captions_truncated": 0, "primary_metric": "crnn exact (case-sensitive)", "per_sample": [], "summary": {}} | |
| from conditioning import MAX_LEN | |
| res["captions_truncated"] = sum(len(c) > MAX_LEN for c in caps) | |
| t0 = time.time() | |
| for steps in a.steps: | |
| imgs[f"s{steps}"] = [to_img(z) for z in student_onnx(te, un, caps, seeds, steps)] | |
| imgs["ref"] = reference_images(g) | |
| preds = {k: ocr_all(v, lays, crnn, tess) for k, v in imgs.items()} | |
| for i, (sp, cap, seed) in enumerate(g): | |
| row = dict(sp, caption=cap, seed=seed) | |
| for k in imgs: | |
| row[k] = {m: dict(ocr=p[i], **score_text(p[i], sp["text"])) for m, p in preds[k].items()} | |
| res["per_sample"].append(row) | |
| for k in imgs: | |
| res["summary"][k] = {m: summarize([{"x": r[k][m]} for r in res["per_sample"]], "x") for m in preds[k]} | |
| res["seconds"] = round(time.time() - t0, 1) | |
| json.dump(res, open(os.path.join(a.assets, "eval_ocr_v3.json"), "w"), indent=1) | |
| for steps in a.steps: | |
| lab = [f"'{sp['text']}' -> '{r[f's{steps}']['crnn']['ocr']}'\n{sp['expr']}/{sp['layout']}/{sp['paper']}" | |
| for (sp, _, _), r in zip(g, res["per_sample"])] | |
| grid_png(imgs[f"s{steps}"], lab, 8, os.path.join(a.assets, f"samples_{steps}step_v3.png"), | |
| title=f"grinpress v3 student ({os.path.basename(a.unet)}), {steps} steps, seeds 1000+i (label: text -> CRNN read)") | |
| print(json.dumps(res["summary"], indent=1)) | |
| # ---- 14-string teacher vs student comparison | |
| g14 = grid14(); c14 = [c for _, c, _ in g14]; s14 = [s for *_, s in g14]; l14 = [sp["layout"] for sp, _, _ in g14] | |
| rows = {"ref": reference_images(g14)} | |
| if a.teacher: | |
| rows["teacher"] = [to_img(z) for z in teacher_images(a.teacher, c14, s14, a.teacher_steps, a.cfg, device=a.device)] | |
| for steps in a.steps: | |
| rows[f"s{steps}"] = [to_img(z) for z in student_onnx(te, un, c14, s14, steps)] | |
| cmp = {} | |
| for k, ims in rows.items(): | |
| p = ocr_all(ims, l14, crnn, tess) | |
| cmp[k] = {m: dict(summarize([{"x": score_text(o, sp["text"])} for o, (sp, _, _) in zip(v, g14)], "x"), ocr=v) for m, v in p.items()} | |
| json.dump(cmp, open(os.path.join(a.assets, "eval_compare_v3.json"), "w"), indent=1) | |
| all_imgs, labels = [], [] | |
| for k, ims in rows.items(): | |
| for i, im in enumerate(ims): | |
| all_imgs.append(im); labels.append(f"'{g14[i][0]['text']}' {k}\n-> '{cmp[k]['crnn']['ocr'][i]}'") | |
| grid_png(all_imgs, labels, len(g14), os.path.join(a.assets, "teacher_vs_student_v3.png"), cell=160, | |
| title="rows: " + " | ".join(rows) + " (same noise per column; label: CRNN read)") | |
| print(json.dumps({k: {m: {x: y for x, y in v.items() if x != "ocr"} for m, v in d.items()} for k, d in cmp.items()}, indent=1)) | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| sub = ap.add_subparsers(dest="cmd", required=True) | |
| r = sub.add_parser("refs") | |
| r.add_argument("--crnn", default=os.path.join(ROOT, "artifacts", "crnn.safetensors")) | |
| r.add_argument("--out", default=os.path.join(ROOT, "assets", "eval_ocr_refs_phase2.json")) | |
| m = sub.add_parser("models") | |
| m.add_argument("--unet", required=True) | |
| m.add_argument("--text", required=True) | |
| m.add_argument("--teacher", default="") | |
| m.add_argument("--teacher_steps", type=int, default=50) | |
| m.add_argument("--cfg", type=float, default=5.0) | |
| m.add_argument("--steps", type=int, nargs="+", default=[4, 2]) | |
| m.add_argument("--crnn", default=os.path.join(ROOT, "artifacts", "crnn.safetensors")) | |
| m.add_argument("--tess", default="v3", help="comma list of tesseract variants to also run (v2,v3,v3s) or ''") | |
| m.add_argument("--assets", default=os.path.join(ROOT, "assets")) | |
| m.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu") | |
| a = ap.parse_args() | |
| torch.set_num_threads(os.cpu_count()) | |
| {"refs": cmd_refs, "models": cmd_models}[a.cmd](a) | |
| if __name__ == "__main__": | |
| main() | |