File size: 5,899 Bytes
4a393d1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# synthetic OCR samples: render text with PIL, return (PIL image, messages)
import random, string, os, glob, json, re
from PIL import Image, ImageDraw, ImageFont, ImageFilter
FONTS=[f for f in glob.glob("/usr/share/fonts/truetype/**/*.ttf",recursive=True) if re.search(r"DejaVu(Sans|Serif|SansMono)(-Bold)?\.ttf|Liberation(Sans|Serif|Mono)-(Regular|Bold)\.ttf|Ubuntu(Mono)?\[wght\]|NotoSansMono-Regular|UbuntuSans\[",f)]
assert FONTS, "no fonts"
PROMPTS=["What does the text in the image say?","Read the text in this image.","OCR this image.","Transcribe the text in the image.","What is written in this image?","Extract the text from the image.","Please read this image."]
_sent=None
def sentences():
    global _sent
    if _sent is None:
        _sent=[]
        for l in open("data/rendered/smoltalk_eval.jsonl"):
            t=json.loads(l)["text"]
            for m in re.finditer(r"<\|im_start\|>(?:user|assistant)\n(.*?)<\|im_end\|>",t,re.S):
                for s in re.split(r"(?<=[.!?])\s+|\n+",m.group(1)):
                    s=s.strip()
                    if 3<=len(s)<=90 and s.isprintable() and s.isascii() and not s.startswith(("<","#","```","-","*","|")): _sent.append(s)
            if len(_sent)>200000: break
    return _sent
def rand_word(rng,n): return "".join(rng.choice(string.ascii_letters+string.digits) for _ in range(n))
def rand_text(rng):
    k=rng.random()
    if k<0.45: return rng.choice(sentences())
    if k<0.60:  # multi-line paragraph
        return "\n".join(rng.choice(sentences()) for _ in range(rng.randint(2,4)))
    if k<0.70: return str(rng.randint(0,10**rng.randint(1,9)))
    if k<0.78: return f"{rng.randint(1,12):02d}/{rng.randint(1,28):02d}/{rng.randint(1990,2030)}"
    if k<0.85: return (rand_word(rng,rng.randint(4,12)) if rng.random()<0.5 else " ".join(rng.choice(sentences()).split()[:rng.randint(1,3)])).upper()
    if k<0.92: return f"{rand_word(rng,rng.randint(3,8)).lower()}@{rng.choice(['example.com','mail.org','test.net'])}"
    if k<0.94: return " ".join(rng.choice(sentences()).split()[:rng.randint(1,4)])
    if k<0.98: return rng.choice([f"{rand_word(rng,rng.randint(3,6)).lower()} {rng.randint(1000,2999)}",f"{rand_word(rng,rng.randint(2,5)).upper()}-{rng.randint(10,999)}",f"{rand_word(rng,rng.randint(3,6))}{rng.randint(1,99)}"])
    return f"${rng.randint(1,9999)}.{rng.randint(0,99):02d}"
def render(rng,text):
    fpath=rng.choice(FONTS); size=rng.randint(16,40)
    lines=text.split("\n")
    pad=rng.randint(4,24)
    dummy=ImageDraw.Draw(Image.new("RGB",(1,1)))
    while True:
        font=ImageFont.truetype(fpath,size)
        ws=[dummy.textbbox((0,0),l,font=font)[2] for l in lines]; hs=[dummy.textbbox((0,0),l,font=font)[3] for l in lines]
        if max(ws)+2*pad<=768 or size<=10: break
        size-=2
    lh=max(hs)+rng.randint(0,8); W=max(ws)+2*pad; H=lh*len(lines)+2*pad
    W=min(max(W,32),768); H=min(max(H,32),512)
    bg=tuple(rng.randint(200,255) for _ in range(3)) if rng.random()<0.8 else tuple(rng.randint(0,60) for _ in range(3))
    fg=tuple(rng.randint(0,70) for _ in range(3)) if sum(bg)>380 else tuple(rng.randint(190,255) for _ in range(3))
    img=Image.new("RGB",(W,H),bg); d=ImageDraw.Draw(img)
    for i,l in enumerate(lines): d.text((pad,pad+i*lh),l,font=font,fill=fg)
    if rng.random()<0.3: img=img.filter(ImageFilter.GaussianBlur(rng.uniform(0.3,1.0)))
    if rng.random()<0.2: img=img.rotate(rng.uniform(-3,3),expand=False,fillcolor=bg)
    return img
def make_sample(rng):
    text=rand_text(rng)
    img=render(rng,text)
    parts=[{"type":"image"},{"type":"text","text":rng.choice(PROMPTS)}]
    if rng.random()<0.5: parts.reverse()
    msgs=[{"role":"user","content":parts},{"role":"assistant","content":text}]
    return img,msgs
if __name__=="__main__":
    rng=random.Random(0); os.makedirs("ocr_samples",exist_ok=True)
    for i in range(8):
        img,m=make_sample(rng); img.save(f"ocr_samples/{i}.png"); print(img.size,repr(m[1]["content"]))
    print(len(FONTS),"fonts")

# ---- preprocessing that mirrors llama.cpp (mtmd) for Qwen3-VL: smart_resize target + align-corners bilinear
import numpy as np
from transformers.models.qwen2_vl.image_processing_qwen2_vl import smart_resize
def lcpp_bilinear(img, W, H):
    src=np.asarray(img.convert("RGB")).astype(np.float32); h,w=src.shape[:2]
    xr=(w-1)/(W-1) if W>1 else 0.0; yr=(h-1)/(H-1) if H>1 else 0.0
    px=np.arange(W)*xr; py=np.arange(H)*yr
    x0=np.minimum(px.astype(np.int64),w-1); y0=np.minimum(py.astype(np.int64),h-1)
    x1=np.minimum(x0+1,w-1); y1=np.minimum(y0+1,h-1)
    xf=(px-x0)[None,:,None]; yf=(py-y0)[:,None,None]
    out=(src[y0[:,None],x0[None,:]]*(1-xf)*(1-yf)+src[y0[:,None],x1[None,:]]*xf*(1-yf)+src[y1[:,None],x0[None,:]]*(1-xf)*yf+src[y1[:,None],x1[None,:]]*xf*yf)
    return Image.fromarray(np.clip(np.round(out),0,255).astype(np.uint8))
def target_size(img, min_pixels=8*1024, max_pixels=4096*1024):
    h,w=smart_resize(img.height,img.width,factor=32,min_pixels=min_pixels,max_pixels=max_pixels); return w,h
def letterbox_lcpp(img, W, H):
    # llama.cpp (mtmd, PAD_CEIL): aspect-preserving bilinear resize, centered on a black canvas
    import math
    img=img.convert("RGB"); sc=min(W/img.width,H/img.height)
    nw=min(math.ceil(img.width*sc),W); nh=min(math.ceil(img.height*sc),H)
    canvas=Image.new("RGB",(W,H),(0,0,0)); canvas.paste(lcpp_bilinear(img,nw,nh),((W-nw)//2,(H-nh)//2)); return canvas
def preprocess(img, ip, rng=None, return_tensors="np"):
    # rng=None -> exact llama.cpp preprocessing; with rng -> 75% llama.cpp style, 25% HF-style stretch with random kernels
    W,H=target_size(img)
    if rng is not None and rng.random()<0.25:
        img=img.convert("RGB").resize((W,H),rng.choice([Image.BILINEAR,Image.BICUBIC,Image.NEAREST,Image.LANCZOS]))
    else:
        img=letterbox_lcpp(img,W,H)
    return ip(images=[img],do_resize=False,return_tensors=return_tensors)