| |
| """Standalone runtime for the exported reader. |
| |
| python inference.py --model-dir . --image receipt.jpg \ |
| --question "๊ฐ๊ฒ ์ ํ๋ฒํธ์ ๋ค์์ 2๋ฒ์งธ ์ซ์๋ ๋ฌด์์
๋๊น?" |
| |
| The graph runs once and produces a record. Questions are answered from that |
| record by `question_router`, so several questions about the same image cost one |
| forward pass -- pass `--question` more than once to see it. |
| |
| Requires onnxruntime, numpy, and Pillow. No tokenizer, no vocabulary. |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| from pathlib import Path |
|
|
| import numpy as np |
|
|
| from question_router import ( |
| address_op_from_question, answer_from_record, phone_op_from_question, |
| route_family_from_question, |
| ) |
|
|
|
|
| def load_config(model_dir: Path) -> dict: |
| return json.loads((model_dir / "config.json").read_text(encoding="utf-8")) |
|
|
|
|
| def preprocess(image_path: Path, cfg: dict) -> np.ndarray: |
| from PIL import Image |
|
|
| spec = cfg["input"] |
| img = Image.open(image_path).convert("L").resize( |
| (int(spec["width"]), int(spec["height"])), Image.Resampling.BILINEAR |
| ) |
| arr = np.asarray(img, dtype=np.float32) / 255.0 |
| return ((arr - 0.5) / 0.5)[None, None, :, :] |
|
|
|
|
| def decode(logits: np.ndarray, cfg: dict) -> dict: |
| blank = int(cfg["blank_class"]) |
| phone_slots = int(cfg["phone_slots"]) |
| ids = logits[0].argmax(-1) |
|
|
| def run(values) -> str: |
| out = [] |
| for v in values: |
| if int(v) == blank: |
| break |
| out.append(str(int(v))) |
| return "".join(out) |
|
|
| return {"phone": run(ids[:phone_slots]), "street": run(ids[phone_slots:])} |
|
|
|
|
| def main() -> int: |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--model-dir", default=".") |
| ap.add_argument("--image", required=True) |
| ap.add_argument("--question", action="append", default=[]) |
| ap.add_argument("--precision", choices=["fp32", "int8"], default="fp32") |
| ap.add_argument("--threads", type=int, default=1) |
| args = ap.parse_args() |
|
|
| import onnxruntime as ort |
|
|
| model_dir = Path(args.model_dir) |
| cfg = load_config(model_dir) |
| name = "model.onnx" if args.precision == "fp32" else "model_int8.onnx" |
| path = model_dir / name |
| if not path.exists(): |
| raise SystemExit(f"missing {path}") |
|
|
| opts = ort.SessionOptions() |
| opts.intra_op_num_threads = args.threads |
| opts.inter_op_num_threads = args.threads |
| sess = ort.InferenceSession(str(path), opts, providers=["CPUExecutionProvider"]) |
|
|
| logits = sess.run(None, {"image": preprocess(Path(args.image), cfg)})[0] |
| record = decode(logits, cfg) |
|
|
| answers = [] |
| for q in args.question: |
| family = route_family_from_question(q) |
| if family == "phone": |
| op = phone_op_from_question(q) |
| elif family == "address": |
| op = address_op_from_question(q) |
| else: |
| op = "unsupported" |
| answers.append({ |
| "question": q, |
| "family": family, |
| "op": op, |
| "answer": answer_from_record(q, record["phone"], record["street"]), |
| }) |
|
|
| print(json.dumps({ |
| "image": args.image, |
| "precision": args.precision, |
| "record": record, |
| "forward_passes": 1, |
| "answers": answers, |
| }, ensure_ascii=False, indent=2)) |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|