sakha-ocr / src /synth.py
loalkota's picture
Детектор строк и распознаватель якутского текста с рецептом обучения
d4d4e7b verified
Raw History Blame Contribute Delete
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)