Download src/synth.py from lab-ii/sakha-ocr: direct link, hf CLI and curl.
- Browser
- Download file 4.58 kB
-
https://huggingface.co/lab-ii/sakha-ocr/resolve/main/src/synth.py
- Command line
-
hf download hf://lab-ii/sakha-ocr/src/synth.py
-
curl -L -o synth.py https://huggingface.co/lab-ii/sakha-ocr/resolve/main/src/synth.py
4.58 kB
| """Генератор синтетических якутских строк/страниц. | |
| Два домена: 'book' (чистая современная книга) и 'news1940' (убитая высокая печать). | |
| """ | |
| import json, os, random, math | |
| import numpy as np | |
| from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageOps | |
| SAH_EXTRA = "ҔҕҤҥӨөҺһҮү" | |
| # --- отбор шрифтов: только те, что реально покрывают алфавит --- | |
| def load_fonts(kind): | |
| rows = json.load(open("data/fonts/ok_fonts.json")) | |
| rows = [r for r in rows if r["full"]] | |
| if kind == "book": | |
| want = ["PT Serif", "Times New Roman", "Charter", "Iowan Old Style", | |
| "Superclarendon", "PT Sans", "Open Sans", "Arial", "Tahoma"] | |
| else: # газета — узкие/плотные гротески и серифы | |
| want = ["PT Serif", "Times New Roman", "Charter", "Superclarendon", | |
| "PT Sans", "Open Sans Cond", "DIN Condensed", "Arial"] | |
| out = [r for r in rows if any(w.lower() in r["name"].lower() for w in want)] | |
| assert out, "нет подходящих шрифтов" | |
| return out | |
| # --- проверка: шрифт реально рисует спец-буквы, а не .notdef --- | |
| def renders_extra(fontpath, idx, size=40): | |
| f = ImageFont.truetype(fontpath, size, index=idx) | |
| ref = None | |
| for ch in SAH_EXTRA: | |
| im = Image.new("L", (size*2, size*2), 255) | |
| ImageDraw.Draw(im).text((5, 5), ch, font=f, fill=0) | |
| a = np.array(im) | |
| if a.min() > 200: # ничего не нарисовалось | |
| return False | |
| # все спец-буквы одинаковые => фолбэк на один и тот же tofu | |
| sig = a.tobytes() | |
| if ref is not None and sig == ref: return False | |
| ref = sig | |
| return True | |
| def degrade(img, domain, rng, _try=0): | |
| """Симуляция скана. img — L-mode. Гарантируем, что текст не выжжен.""" | |
| src = img | |
| a = np.asarray(img).astype(np.float32) / 255.0 | |
| if domain == "news1940": | |
| # 1) неровная прокраска: низкочастотный шум по яркости | |
| h, w = a.shape | |
| lf = rng.normal(0, 1, (max(2,h//24), max(2,w//24))).astype(np.float32) | |
| lf = np.array(Image.fromarray(lf).resize((w, h), Image.BICUBIC)) | |
| a = a + lf * rng.uniform(0.05, 0.18) | |
| # 2) растекание/разрыв штриха | |
| img2 = Image.fromarray(np.clip(a*255,0,255).astype(np.uint8)) | |
| if rng.random() < 0.6: | |
| img2 = img2.filter(ImageFilter.MinFilter(3)) # ink spread (темнее) | |
| if rng.random() < 0.45: | |
| img2 = img2.filter(ImageFilter.MaxFilter(3)) # разрыв штриха | |
| a = np.asarray(img2).astype(np.float32)/255. | |
| # 3) серо-жёлтая бумага + просвет с обратной стороны | |
| a = a * rng.uniform(0.80, 0.95) + rng.uniform(0.05, 0.18) | |
| blur = rng.uniform(0.15, 0.7) # высокая печать даёт жёсткий край, не мыло | |
| else: | |
| a = a * rng.uniform(0.92, 1.0) + rng.uniform(0.0, 0.06) | |
| blur = rng.uniform(0.2, 0.9) | |
| img = Image.fromarray(np.clip(a*255, 0, 255).astype(np.uint8)) | |
| img = img.filter(ImageFilter.GaussianBlur(blur)) | |
| a = np.asarray(img).astype(np.float32) | |
| a += rng.normal(0, rng.uniform(3, 14) if domain=="news1940" else rng.uniform(1,6), a.shape) | |
| img = Image.fromarray(np.clip(a,0,255).astype(np.uint8)) | |
| if rng.random() < 0.7: # перекос | |
| img = img.rotate(rng.uniform(-0.8, 0.8), resample=Image.BICUBIC, fillcolor=235, expand=False) | |
| # контроль: контраст текст/фон не должен схлопнуться | |
| a = np.asarray(img).astype(np.float32) | |
| ink = float(np.percentile(a, 2)); paper = float(np.percentile(a, 90)) | |
| if paper - ink < 55 and _try < 4: | |
| return degrade(src, "book" if _try >= 2 else domain, rng, _try + 1) | |
| return img | |
| def render_line(text, font, pad=10, rng=None, domain="book"): | |
| f = ImageFont.truetype(font["path"], font["size"], index=font["idx"]) | |
| tmp = Image.new("L", (10,10), 255) | |
| box = ImageDraw.Draw(tmp).textbbox((0,0), text, font=f) | |
| W, H = box[2]-box[0]+2*pad, box[3]-box[1]+2*pad | |
| im = Image.new("L", (W, H), 255) | |
| ImageDraw.Draw(im).text((pad-box[0], pad-box[1]), text, font=f, fill=rng.integers(0,45)) | |
| return degrade(im, domain, rng) | |