Reconnaissance de texte manuscrit : un pipeline de pré-rendu Python robuste pour les ensembles de données de texte manuscrit (IAM) en ligne avec des augmentations numpy

Charles Gaillard

Charles Gaillard

Mohamed Biaz

Mohamed Biaz

Détection de mots OCR

Hors ligne ensembles de données de reconnaissance de texte manuscrit (images numérisées optiquement), par opposition à en ligne les jeux de données de reconnaissance manuscrite (enregistrement de la trajectoire du stylo en fonction du temps) ne contiennent pas d'images mais des traits. Nous allons essayer d'expliquer comment créer un pipeline de pré-rendu pour l'écriture manuscrite en ligne qui peut être utilisé pour la formation aux modèles de reconnaissance de texte en python.

Un trait est une liste de triplets (x, y, t) où (x, y) sont les coordonnées 2D des points et (t) est le temps de dessin collecté par l'écran sensible, comme un appareil doté d'un écran tactile.

Lors de la formation d'un modèle de reconnaissance de texte, nous envisageons généralement d'utiliser des ensembles de données contenant des images, car nous utilisons des modèles basés sur la vision. C'est pourquoi, la plupart du temps, les ingénieurs en deep learning s'orientent vers des ensembles de données hors ligne et entraînent simplement leurs modèles à l'aide d'images et d'étiquettes directement à partir de l'ensemble de données, avec des augmentations d'images.

Nous devons surmonter cette dépendance à l'égard des images : l'entraînement d'un modèle basé sur la vision en prenant des images en entrée ne nécessite pas nécessairement un jeu de données d'images. Les ensembles de données en ligne contiennent une énorme quantité de données précieuses, qui peuvent être facilement exploitées et converties instantanément en images. De plus, l'accès aux points bruts de chaque trait de chaque mot, lors de l'utilisation de jeux de données en ligne, nous permet d'effectuer de nombreuses NumPy opérations directement sur ces points.

Dans cet article, nous fournirons un pipeline de transformation Python complet pour les ensembles de données manuscrits en ligne à l'aide de GIA, en commençant par les points de données (traits) jusqu'au rendu de l'image. Il comprendra une collection d'augmentations Numpy simples et rapides effectuées directement sur les traits et les points.

Rendu d'image pour les ensembles de données de reconnaissance manuscrite en ligne à l'aide d'IAM

Avant de commencer, il est important de noter que ces opérations sont effectuées sur des points et non sur des images, ce qui les rend extrêmement rapides et ne nécessite qu'une dépendance à Numpy.

Les données texte en ligne IAM sont fournies sous forme de fichier XML. Nous devons l'analyser pour obtenir les traits. Vous trouverez ci-dessous un extrait de code Python expliquant comment analyser un point de données XML du jeu de données hors ligne IAM :

import xml.etree.ElementTree as ET
import numpy as np
import random

def parse_strokes(xml_path: str) -> List[np.ndarray]:
"""Parse a XML file from the IAM online dataset, returns a list of strokes (each one is an array of 2D points)"""
tree = ET.parse(xml_path)
root = tree.getroot()
strokes = [
[
(
int(point.attrib["x"]),
int(point.attrib["y"])
)
for point in stroke
]
for stroke in root[-1]
]
return [np.asarray(stroke) for stroke in strokes]

Si nous dessinons simplement les points sur une toile blanche, nous obtenons le rendu brut présenté dans l'image ci-dessous. Pour clarifier le code, les exemples de code suivants ne contiennent que les manipulations de points : le dessin du canevas sera présenté plus loin dans l'article. Notez que pour les exemples suivants, nous utiliserons le premier point de données du jeu de données en ligne IAM (lineStrokes-all/lineStrokes/a01/a01-000/a01-000u-01.xml).

Points bruts du premier point de données d'IAM en ligne tracés sur une toile blanche

C'est aussi simple que cela, mais ne nous arrêtons pas là. Nous pouvons augmenter la résolution des points de manière aléatoire, pour éviter « l'effet dashlane » (points au lieu de lignes) et mieux distinguer les lettres :

def random_enrich_strokes(
strokes: List[np.ndarray],
max_factor: int = 3
) -> List[np.ndarray]:
"""Multiply by until 2 * max_factor the number of points in the strokes to have a better resolution."""
for _ in range(random.randint(1, max_factor)):
strokes = [
np.concatenate(
(p, [(p[i] + p[i + 1]) / 2 for i in range(len(p) - 1)]),
axis=0
)
for p in strokes
]
return strokes

Voici comment cela s'affiche si nous multiplions le nombre de points par un facteur de 2 :

Points bruts du premier point de données d'IAM en ligne tracés sur une toile blanche avec enrichissement à deux points

Plus nous ajoutons de points au canevas, plus il ressemble à une ligne unie. Ceci est important si vous souhaitez entraîner un modèle de reconnaissance de texte manuscrit, car il correspond mieux à une distribution de données réelle. Voici une illustration permettant de comparer les deux toiles sans enrichissement et avec un facteur de 2 :

Comparaison d'enrichissement du canevas tracé en ligne IAM

Ajout d'une augmentation simulant un texte manuscrit réel

Faisons maintenant une dilatation aléatoire (en les espaçant) sur chaque trait pour déplacer les lettres de manière relative.

def random_dilate_strokes(
strokes: List[np.ndarray], x_d: float = 1e-3, y_d: float = 7e-2
) -> List[np.ndarray]:
"""Perform random vertical dilation on each stroke."""
# Compute random dilation parameters
y_dil = [random.uniform(1 - y_d, 1 + y_d) for _ in strokes]
x_dil = [random.uniform(1 - x_d, 1 + x_d) for _ in strokes]
return [
[(int(x_dil[i] * x), int(y_dil[i] * y)) for (x, y) in stroke]
for i, stroke in enumerate(strokes)
]

Voici comment cela s'affiche :

Points IAM avec dilatation : les lettres sont décalées de manière aléatoire en Y et en X

Pour opérer sur tous les points, aplatissons les traits dans un tableau de points :

def flatten_strokes(strokes: List[np.ndarray]) -> np.ndarray:
"""Flatten a list of strokes in an array of points"""
return np.asarray([p for stroke in strokes for p in stroke], np.int32)

Appliquons maintenant une transformation aléatoire à nos points. L'objectif est de créer de la variabilité dans nos données sans ajouter de nouveaux échantillons. Nous allons ajouter deux transformations, tondre, et rotation:

Augmentation de l'image par cisaillement et rotation
def random_transform_points(
points: np.ndarray, rot: float = 0.1, shear: float = 0.5
) -> np.ndarray:
"""Randomly transform 2D points"""
transform = np.asarray(
[
[random.uniform(1 - shear, 1 + shear), random.uniform(-rot, rot)],
[random.uniform(-rot, rot), random.uniform(1 - shear, 1 + shear)],
],
np.float32,
)
return np.matmul(points, transform)

Nous avons 2 exemples de rendu ici :

Les points sont multipliés par une matrice de transformation 2D avant d'être tracés sur la toile blanche

Redimensionnons les points :

def resize_points(
points: np.ndarray, dwn_size: int = 10, shift: int = 10
) -> np.ndarray:
"""Downsize to have a decent image size, and shift to see full characters"""
points = points / dwn_size
points[:, 0] -= np.min(points[:, 0])
points[:, 1] -= np.min(points[:, 1])
return points + shift

Maintenant, toutes ces manipulations peuvent ne pas être utiles si nous ne les rendons pas. Nous allons donc maintenant calculer un canevas Numpy sur lequel dessiner les points :

def compute_random_canvas(points: np.ndarray, shift: int = 10, noise: float = .5, light: float = .3) -> np.ndarray:
"""Compute the RGB canvas to fit the points."""
h, w = 2 * shift + int(np.max(points[:, 1])), 2 * shift + int(np.max(points[:, 0]))
# Compute canvas mode: uniform color or rainbow
canvas = (np.tile(np.arange(w), (h, 1)) / w) if random.random() > 0.5 else np.ones((h, w))
# Random reverse and roll each RGB channel
canvas = np.stack(
(
np.roll(canvas[..., ::-1], random.randint(0, w), 1) if random.random() > 0.5 else canvas,
np.roll(canvas[..., ::-1], random.randint(0, w), 1) if random.random() > 0.5 else canvas,
np.roll(canvas[..., ::-1], random.randint(0, w), 1) if random.random() > 0.5 else canvas,
),
axis=-1,
)
# Lighten
light = light * np.ones(canvas.shape)
# Compute noise
noise = random.uniform(0, noise) * np.random.rand(*canvas.shape)
return light + canvas - noise

Voici des exemples de canevas générés :

Mêmes points de données IAM tracés sur différents canevas générés de manière aléatoire

Enfin, affichons nos points sur le canevas :

def random_draw(canvas: np.ndarray, points: np.ndarray, shift: int = 3, density: int = 20) -> np.ndarray:
"""Draw points with random local shifts and random colors on canvas.
"""
uniform_color = (random.random(), random.random(), random.random()) if random.random() > .5 else None
uniform_shift = np.random.randint(1, shift) if random.random() > .5 else None
for point in points:
x, y = point
color = uniform_color if uniform_color else (random.random(), random.random(), random.random())
if uniform_shift:
for i in range(uniform_shift):
for j in range(uniform_shift):
canvas[int(y) + i, int(x) + j] = color
else:
for _ in range(random.randint(1, density)):
canvas[int(y) + random.randint(0, shift), int(x) + random.randint(0, shift)] = color
return canvas

Voici quelques exemples avec des variantes de dessin :

Même point de données tracé avec un bruit aléatoire et un décalage autour de points et de couleurs différentes

À partir de là, il est plus facile de créer un pipeline d'augmentation générative, en utilisant un chemin de fichier comme entrée et en restituant des versions augmentées aléatoires du point de données d'origine à partir de l'ensemble de données en ligne IAM :

def random_augment(filepath: str, n_samples: int = 100):
for _ in range(n_samples):
strokes = parse_strokes(filepath)
# Operations on strokes
strokes = random_enrich_strokes(strokes)
strokes = random_dilate_strokes(strokes)
points = flatten_strokes(strokes)
# Operations on points
points = random_transform_points(points)
points = resize_points(points)
# Draw on canvas
canvas = compute_random_canvas(points)
canvas = random_draw(canvas, points)

L'image suivante contient 10 échantillons générés aléatoirement avec l'extrait de code précédent :

Augmentations aléatoires avec bruit sur les points de données IAM

On peut jouer avec les paramètres de chaque fonction du pipeline pour modifier les transformations.

Conclusion

Les ensembles de données manuscrites en ligne peuvent être exploités pour générer de nombreux échantillons d'images très différents avec de simples augmentations. Comme vous manipulez des points au lieu d'images, c'est beaucoup plus rapide que d'utiliser des ensembles de données hors ligne, et nous ne mentionnons même pas la taille du jeu de données à télécharger. En fin de compte, c'est rapide et facile, et cela aidera sûrement votre modèle de reconnaissance de texte manuscrit à converger si vous utilisez cet ensemble de données augmenté.

À propos

Qu'il s'agisse de simples photos, de fichiers PDF complexes ou de fichiers manuscrits, l'API de Mindee transforme les données de vos documents en JSON structuré de manière hautement fiable. Aucune formation sur les modèles n'est requise. Tous les alphabets et toutes les langues sont pris en charge.

,
,

Key Takeway

Key Takeway