Instructions to use thejagstudio/portfolio-models with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers.js
How to use thejagstudio/portfolio-models with Transformers.js:
// ⚠️ Unknown pipeline tag
File size: 5,409 Bytes
139ea68 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | """Reference of the browser OCR pipeline (src/lib/docai/ocr.js) on PP-OCRv5 mobile det + en_PP-OCRv5 mobile rec.
det: longest side resized to LIMIT (multiple of 32), BGR, ImageNet mean/std -> probability map;
threshold 0.3 -> connected components -> axis-aligned boxes scored by mean probability (>= 0.6),
grown by area * 1.5 / perimeter (DB's unclip for a rectangle)
rec: each box cropped, height 48, width bucketed, (x/255 - 0.5) / 0.5, BGR -> CTC greedy decode;
each kept character's time step gives its x position, so lines split into word boxes.
Run with .models/.venv311mp (opencv, onnxruntime). usage: python ocr_reference.py [image]
"""
import os, sys, time
import numpy as np
import cv2
import onnxruntime as ort
from PIL import Image, ImageDraw, ImageFont
HERE = os.path.dirname(os.path.abspath(__file__))
PORT = os.path.join(HERE, "..", "portfolio", "pp-ocrv5")
DICT = open(os.path.join(PORT, "en_dict.txt"), encoding="utf-8").read().split("\n")[:-1] + [" "]
LIMIT, THRESH, BOX_THRESH, UNCLIP = 1280, 0.3, 0.6, 1.5
MEAN, STD = np.array([0.485, 0.456, 0.406], np.float32), np.array([0.229, 0.224, 0.225], np.float32)
def synthetic_invoice():
img = Image.new("RGB", (1240, 1600), "white")
d = ImageDraw.Draw(img)
f = lambda s, b=False: ImageFont.truetype("C:/Windows/Fonts/arialbd.ttf" if b else "C:/Windows/Fonts/arial.ttf", s)
d.text((80, 80), "ACME Supplies Ltd.", font=f(44, True), fill="black")
d.text((900, 90), "INVOICE", font=f(52, True), fill=(240, 72, 44))
d.text((80, 150), "221B Baker Street, London NW1 6XE", font=f(24), fill="#333")
for i, (k, v) in enumerate([("Invoice #:", "INV-2024-0412"), ("Date:", "12 March 2024"), ("Due:", "11 April 2024")]):
d.text((820, 200 + i * 36), k, font=f(24, True), fill="black")
d.text((960, 200 + i * 36), v, font=f(24), fill="black")
d.text((80, 360), "Description", font=f(26, True), fill="black")
d.text((900, 360), "Amount", font=f(26, True), fill="black")
for i, (k, v) in enumerate([("Printer paper A4 (10 reams)", "$45.90"), ("Toner cartridge HP 26A", "$129.00"), ("Delivery", "$12.50")]):
d.text((80, 420 + i * 48), k, font=f(26), fill="black")
d.text((900, 420 + i * 48), v, font=f(26), fill="black")
d.text((700, 620), "Total due: $187.40", font=f(34, True), fill="black")
d.text((80, 760), "Thank you for your business! Questions? billing@acme.co.uk", font=f(20), fill="#555")
return np.array(img)[:, :, ::-1] # BGR
def detect(sess, bgr):
h, w = bgr.shape[:2]
s = min(1.0, LIMIT / max(h, w)) if max(h, w) > LIMIT else LIMIT / max(h, w)
rh, rw = max(32, int(round(h * s / 32)) * 32), max(32, int(round(w * s / 32)) * 32)
x = cv2.resize(bgr, (rw, rh)).astype(np.float32) / 255
x = ((x - MEAN) / STD).transpose(2, 0, 1)[None]
prob = sess.run(None, {"x": x})[0][0, 0]
n, labels, stats, _ = cv2.connectedComponentsWithStats((prob > THRESH).astype(np.uint8), connectivity=8)
boxes = []
for i in range(1, n):
x0, y0, bw, bh, area = stats[i]
if min(bw, bh) < 3:
continue
score = float(prob[labels == i].mean())
if score < BOX_THRESH:
continue
d = bw * bh * UNCLIP / (2 * (bw + bh))
sx, sy = w / rw, h / rh
boxes.append([max(0, (x0 - d) * sx), max(0, (y0 - d) * sy), min(w, (x0 + bw + d) * sx), min(h, (y0 + bh + d) * sy), score])
boxes.sort(key=lambda b: (round(b[1] / 20), b[0]))
return boxes
def recognize(sess, bgr, boxes):
lines = []
for x0, y0, x1, y1, _ in boxes:
crop = bgr[int(y0):int(np.ceil(y1)), int(x0):int(np.ceil(x1))]
ch, cw = crop.shape[:2]
tw = max(16, int(np.ceil(48 * cw / ch)))
x = cv2.resize(crop, (tw, 48)).astype(np.float32) / 255
x = ((x - 0.5) / 0.5).transpose(2, 0, 1)[None]
probs = sess.run(None, {"x": x})[0][0] # [T, 438]
T = probs.shape[0]
ids, conf = probs.argmax(1), probs.max(1)
chars, prev = [], 0
for t, k in enumerate(ids):
if k != 0 and k != prev:
chars.append((DICT[k - 1], t, conf[t]))
prev = k
text = "".join(c for c, _, _ in chars)
# words from the character time steps
words, cur = [], []
for c in chars + [(" ", T, 1.0)]:
if c[0] == " ":
if cur:
t0, t1 = cur[0][1], cur[-1][1]
words.append(("".join(k for k, _, _ in cur), x0 + (t0 - 0.5) * cw / T, x0 + (t1 + 1.5) * cw / T, float(np.mean([p for _, _, p in cur]))))
cur = []
else:
cur.append(c)
lines.append((text, float(np.mean([p for _, _, p in chars])) if chars else 0, (x0, y0, x1, y1), words))
return lines
if __name__ == "__main__":
bgr = cv2.imread(sys.argv[1]) if len(sys.argv) > 1 else synthetic_invoice()
det = ort.InferenceSession(os.path.join(PORT, "det.onnx"), providers=["CPUExecutionProvider"])
rec = ort.InferenceSession(os.path.join(PORT, "rec.onnx"), providers=["CPUExecutionProvider"])
t0 = time.time()
boxes = detect(det, bgr)
lines = recognize(rec, bgr, boxes)
print(f"{len(lines)} lines in {time.time() - t0:.2f}s")
for text, conf, box, words in lines:
print(f"{conf:.2f} {[int(v) for v in box]} {text!r} | words: {[(w[0], int(w[1]), int(w[2])) for w in words]}")
|