127 lines
4.5 KiB
Python
127 lines
4.5 KiB
Python
"""
|
|
Synthetic data generator.
|
|
|
|
Maps to the flowchart's "Sintētiskie dati" branch: since we don't have scanned
|
|
handwriting yet, we render Latvian text as images and distort them (rotation,
|
|
shear, noise, blur, varying stroke width) to roughly approximate handwriting
|
|
variability. This is a placeholder, not a replacement for real data - swap in
|
|
scanned samples via `RealDataset` in dataset.py as soon as you have them.
|
|
|
|
For much better realism, drop a handwriting-style .ttf (e.g. any free cursive/
|
|
print handwriting font that supports Latvian diacritics: ā č ē ģ ī ķ ļ ņ š ū ž)
|
|
into FONT_DIR and it will be picked up automatically.
|
|
"""
|
|
|
|
import glob
|
|
import os
|
|
import random
|
|
|
|
import numpy as np
|
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
|
|
|
FONT_DIR = os.path.join(os.path.dirname(__file__), "fonts")
|
|
FALLBACK_FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
|
|
|
|
TARGET_HEIGHT = 32 # fixed line height fed into the CNN
|
|
|
|
|
|
def _available_fonts() -> list[str]:
|
|
fonts = glob.glob(os.path.join(FONT_DIR, "*.ttf")) + glob.glob(os.path.join(FONT_DIR, "*.otf"))
|
|
return fonts if fonts else [FALLBACK_FONT]
|
|
|
|
|
|
def _default_corpus() -> list[str]:
|
|
"""Small built-in Latvian word/phrase list, used if data/corpus.txt is absent."""
|
|
return [
|
|
"Labdien", "paldies", "lūdzu", "sveiki", "atā", "jā", "nē",
|
|
"Rīga", "Latvija", "valoda", "grāmata", "skola", "pilsēta",
|
|
"saule", "lietus", "sniegs", "vējš", "koks", "upe", "jūra",
|
|
"draugs", "ģimene", "māja", "ceļš", "darbs", "laiks",
|
|
"Es mācos latviešu valodu.", "Šodien ir skaista diena.",
|
|
"Viņš dzīvo Rīgā.", "Mums ir daudz darba.", "Kur ir tuvākā aptieka?",
|
|
"Cik tas maksā?", "Es gribu kafiju.", "Rīt būs saulains laiks.",
|
|
"Bērni spēlējas parkā.", "Šī grāmata ir ļoti interesanta.",
|
|
]
|
|
|
|
|
|
def load_corpus() -> list[str]:
|
|
corpus_path = os.path.join(os.path.dirname(__file__), "data", "corpus.txt")
|
|
if os.path.exists(corpus_path):
|
|
with open(corpus_path, encoding="utf-8") as f:
|
|
lines = [ln.strip() for ln in f if ln.strip()]
|
|
if lines:
|
|
return lines
|
|
return _default_corpus()
|
|
|
|
|
|
def render_text_line(text: str, font_path: str | None = None, augment: bool = True) -> Image.Image:
|
|
"""Render `text` as a single grayscale line image of fixed height."""
|
|
font_path = font_path or random.choice(_available_fonts())
|
|
font_size = random.randint(26, 40)
|
|
font = ImageFont.truetype(font_path, font_size)
|
|
|
|
# Measure text to size the canvas, with padding
|
|
dummy = Image.new("L", (10, 10), color=255)
|
|
bbox = ImageDraw.Draw(dummy).textbbox((0, 0), text, font=font)
|
|
w = max(1, bbox[2] - bbox[0]) + 20
|
|
h = max(1, bbox[3] - bbox[1]) + 20
|
|
|
|
img = Image.new("L", (w, h), color=255)
|
|
draw = ImageDraw.Draw(img)
|
|
draw.text((10 - bbox[0], 10 - bbox[1]), text, font=font, fill=0)
|
|
|
|
if augment:
|
|
img = _augment(img)
|
|
|
|
img = _resize_keep_ratio(img, TARGET_HEIGHT)
|
|
return img
|
|
|
|
|
|
def _augment(img: Image.Image) -> Image.Image:
|
|
# Slight random rotation
|
|
angle = random.uniform(-3, 3)
|
|
img = img.rotate(angle, expand=True, fillcolor=255)
|
|
|
|
# Slight shear via affine transform, mimics slanted handwriting
|
|
shear = random.uniform(-0.25, 0.25)
|
|
w, h = img.size
|
|
img = img.transform(
|
|
(w + int(abs(shear) * h), h),
|
|
Image.AFFINE,
|
|
(1, shear, -shear * h if shear < 0 else 0, 0, 1, 0),
|
|
fillcolor=255,
|
|
)
|
|
|
|
if random.random() < 0.5:
|
|
img = img.filter(ImageFilter.GaussianBlur(radius=random.uniform(0.2, 0.8)))
|
|
|
|
if random.random() < 0.5:
|
|
arr = np.array(img).astype(np.float32)
|
|
noise = np.random.normal(0, random.uniform(3, 10), arr.shape)
|
|
arr = np.clip(arr + noise, 0, 255).astype(np.uint8)
|
|
img = Image.fromarray(arr)
|
|
|
|
return img
|
|
|
|
|
|
def _resize_keep_ratio(img: Image.Image, target_height: int) -> Image.Image:
|
|
w, h = img.size
|
|
new_w = max(1, int(w * (target_height / h)))
|
|
return img.resize((new_w, target_height), Image.BILINEAR)
|
|
|
|
|
|
def generate_batch(n: int, corpus: list[str] | None = None) -> list[tuple[Image.Image, str]]:
|
|
corpus = corpus or load_corpus()
|
|
samples = []
|
|
for _ in range(n):
|
|
text = random.choice(corpus)
|
|
samples.append((render_text_line(text), text))
|
|
return samples
|
|
|
|
|
|
if __name__ == "__main__":
|
|
os.makedirs("samples", exist_ok=True)
|
|
for i, (img, text) in enumerate(generate_batch(500)):
|
|
img.save(f"samples/sample_{i}.png")
|
|
print(f"sample_{i}.png -> {text!r} size={img.size}")
|