Mis à jour le mardi 30 décembre 2014
  • 2 heures
  • Moyenne
Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Introduction du cours

Introduction

Bonjour, bonsoir ; bienvenue ! Si vous avez cliqué sur le lien menant à ce cours, c'est que vous vous êtes un jour posé l'une des questions suivantes :

  • qu'est-ce que l'intelligence artificielle (IA), terme très sci-fi ?

  • qu'est-ce que l'OCR, ce terme très nerdy ?

  • à quoi ça sert, et comment ça marche ?

Je vais ici donc rapidement répondre à ces trois questions. Notez bien le mot "rapidement". Ce terme s'impose puisque chacune des question plus haut est une porte qui ouvre sur un monde gigantesque, et bien plus complexe que ce que je vais vous présenter. Néanmoins, j'espère réellement pouvoir présenter ces domaines de façon claire et simple, d'une manière qui va vous donner envie de vous y intéresser et de pousser les recherches au-delà de cette simple introduction. Si je parviens à réaliser cet objectif, j'en serais très heureux.11

Quoi qu'il en soit, si vous suivez bien le tutoriel, vous finirez par avoir une première idée de comment fonctionne un des algorithmes utilisés par Word ou votre lecteur de PDF favori, par exemple, pour reconnaître les textes présents sur une image et, mieux encore, vous aurez un petit programme qui pourra reconnaître des caractères : un à la fois, certes, mais c'est déjà pas mal !

Ce programme sera écrit en C# et implémentera les idées présentées de la manière la plus simple possible, pour que vous puissiez le porter au langage de votre choix.

Qu'est-ce que l'IA ?

Grosso modo...

Dans ce tutoriel, nous allons nous intéresser au domaine de l'IA ; qu'est-ce que c'est ? Avant tout, il serait peut-être utile de savoir ce que ces lettres signifient.

Le sigle "IA" signie "intelligence artificielle". C'est un domaine dont vous avez sans doute entendu parler, au travers de récits (comme les œuvres de science-fiction, ou des films), ou encore dans le monde du gaming. Les premiers vous ont sans doute présenté la discipline comme un monde de recherche où l'on cherchait à créer un esprit humain, ou du moins conscient et intelligent. Ce n'est pas totalement faux, mais pas complètement vrai non plus.

En effet, le but de l'intelligence artificielle est de donner naissance à des machines plus intelligentes (vous voyez la boucle ?...), dans le sens où elles pourront comprendre ce que nous faisons et pourront, de fait, répondre de façon plus directe et plus efficace à nos besoins. Par exemple, imaginez que vous soyez un médecin et que vous analysiez le scanner d'un patient. Vous doutez sur ce qu'il a et demandez de l'aide à un programme médical. Ce programme doit être entraîné (éduqué) à reconnaître les symptômes et à diagnostiquer les maladies correctement. C'est déjà une première étape, et beaucoup de chercheurs en IA planchent uniquement sur cette partie de la reconnaissance et de l'entraînement des machines. On pourrait simplement résumer cela par la question "comment est-ce que je fais pour enseigner que ces pixels sont un chat à ce PC ?".

Une première étape. Maintenant, ne serait-il pas mieux pour tout le monde si ce logiciel médical pouvait directement converser avec le médecin pour lui présenter son avis concernant le patient ? Il pourrait parler avec une voix de synthèse et comprendre ce que le médecin lui dit à l'aide de la reconnaissance vocale. Encore une fois, c'est un problème de reconnaissance.

Vous l'aurez compris, l'IA tourne beaucoup autour de la reconnaissance d'objets, que ce soit des objets "standards" (au sens, communs), ou moins fréquents, tels que l'angle d'une route (qui fait tourner une voiture) ou une série de gestes  permettant d'accéder à votre base secrète. Dans le jargon, ces objets s'appellent des motifs (de l'anglais pattern).

Au cours du temps, les chercheurs se sont fabriqué un petit arsenal d'outils permettant de faire cette reconnaissance, de façon de plus en plus efficace (et impressionnante, regardez une IA dans un jeu, ou alors les logiciels de diagnostic médicaux, qui sont à la pointe de la technologie dans le domaine).

C'est une science assez libre, puisqu'elle se soucie uniquement d'atteindre ses objectifs. Elle se base souvent sur d'autres sciences pour se développer. Par exemple, elle s'est basée sur la biologie du cerveau humain pour concevoir ce qu'on appelle des réseaux de neurones artificiels. Ces réseaux ont pour but de reconnaître des motifs connus à l'avance, et de pouvoir en apprendre de nouveaux, à l'aide d'un entraînement (ou sans, pour certains modèles). Évidemment, l'IA ne reprend pas tout dans les plus stricts détails, mais elle reprend quelques éléments importants pour atteindre l'objectif. On pourrait dire qu'elle est pragmatique, de ce point de vue là.

L'intelligence artificielle au quotidien, cachée, mais bien présente.

Tout cela peut vous paraître plutôt loin, après toute cette lecture. Vous pourriez vous demander :

Tout ça est bien beau, mais qu'est-ce que l'IA fait pour moi, est-ce que je peux au moins la voir dans ma vie de tous les jours !?

La réponse à cette dernière question est évidemment un grand oui.11

Vous pouvez voir l'IA en action partout autour de vous, si vous regardez bien. Par exemple : Google utilise des techniques d'IA pour faire ses recherches ; Facebook fait de même pour grouper ses utilisateurs entre eux ; les caméras de surveillance peuvent reconnaître des situations à problèmes (ou, du moins, anormales) automatiquement et donner l'alerte ; les robots (comme le gentil petit Nao) utilise plusieurs techniques d'IA de pointe (dont les réseaux neuronaux, cités au-dessus) pour, par exemple, reconnaître à qui il parle, ou comment répondre. Un autre exemple, moins high-tech cette fois, est celui de votre lave-vaisselle (ou lave-linge). Votre machine doit "penser" pour doser les produits nettoyants et l'eau, elle doit aussi "penser" au niveau de saleté qu'elle reçoit lors du calcul pour le dosage du produit. Par exemple, si les assiettes sont plutôt propres, elle n'utilisera pas beaucoup de produit, tandis que si c'est noir-charbon, alors elle ne va pas hésiter une seconde à vider la réserve ! (Sachez, entre nous, qu'un être humain ne résisterait pas à l'envie non plus).

Ah ! C'était une grosse introduction, n'est-ce pas ? Passons maintenant à ce qui nous intéresse vraiment pour ce tutoriel : l'OCR.

L'OCR

Qu'est-ce que c'est ?

On va d'abord s'intéresser à ce qu'est l'OCR, avant de penser à comment implémenter notre propre module.

Le terme "OCR" est un acronyme pour "Optical Character Recognition", ou Reconnaissance de caractère optique, en français. L'OCR est donc un domaine de l'IA qui a pour but de reconnaître un caractère (lettre, chiffre, symbole alphabétique) lorsqu'on lui en présente un. Évidemment, vous pourriez être en train de vous dire que c'est ce qu'un ordinateur fait automatiquement. Après tout, quand j'appuie sur la touche A, je vois un A apparaître. S'il le fait dans un sens, il le fait automatiquement dans l'autre, right ?

Eh bien, non. Reconnaître un caractère depuis une image est le but de l'OCR. En pratique, reconnaître un seul caractère ne va pas nous mener bien loin, et les logiciels professionnels sont bien entendu capables de reconnaître plusieurs lettres mises à côté. C'est par exemple le cas de OneNote, qui peut extraire le texte dans une image lorsqu'on le lui demande. Cependant, reconnaître plusieurs lettres est simplement extraire des pixels donnés d'une image qui pourraient être des lettres et les passer, un  à un, au module d'OCR pour faire la reconnaissance, pour ensuite mettre les lettres les unes à la suite des autres et reformer la phrase vue. Reconnaître plusieurs lettres est en fait plus un travail de traitement d'image que de reconnaissance, puisqu'on ne reconnait qu'une lettre à la fois. C'est sur cette dernière étape que l'on va se pencher ici.

Ce que nous allons faire

Dans ce tutoriel, nous allons construire notre propre module de reconnaissance. Il ne va pas pouvoir reconnaître qu'une lettre à la fois, cependant, comme nous l'avons vu plus haut.

Avant toute chose, réfléchissons à comment nous pourrions faire cela. On peut étudier ce qu'il se passe réellement sur le marché professionnel pour nous donner des idées.

Une rapide recherche nous permet de trouver que la plupart des solutions de reconnaissance professionnelles sont basées sur les réseaux de neurones artificiels, dont on a parlé plus haut. Ces réseaux sont capables d'apprendre d'exemples et peuvent reconnaître même des formes qu'ils n'avaient jamais vu auparavant, tant que la chose n'est pas totalement nouvelle. C'est cette flexibilité qui est attirante chez eux. Cependant, en conçevoir un est une tâche plutôt difficile, et il nous faudrait de quoi l'entraîner (des centaines d'images de chiffres et de lettres écrites à la main ou par un ordinateur pour les montrer au réseau de neurones), ce qui peut être chose difficile à trouver. On va donc oublier les réseaux de neurones ; trop compliqué.

Un autre algorithme, et bien plus simple, puisque c'est le plus simple de tous existe cependant. Cet algorithme est celui de la distance Euclidienne. Commençons par expliquer ce qu'est cette distance avant de voir l'algorithme en lui-même.

La distance euclidienne

En géométrie, un point a des coordonnées spatiales représentées par un vecteur position. La distance euclidienne est simplement la différence entre deux vecteurs, c'est-à-dire la distance qu'il existe entre deux points. Par exemple, la distance entre ma maison et mon lycée est d'environ 650 mètres.

Si vous ne savez pas ce qu'est un vecteur, n'ayez pas peur, c'est un concept très simple. Un vecteur peut être représenté comme un tableau de flottants (j'imagine que tout programmeur qui se respecte sait ce qu'est un flottant...), c'est-à-dire de nombres décimaux. Ces nombres représentent la position d'un point sur les différentes directions de l'espace (les trois fameux axes x, y, z en 3D).

En maths, on représente un vecteur comme ceci :

$$$$$\[\vec a \begin{pmatrix} x\\y\\z \end{pmatrix}\]$$$$$

Évidemment, un vecteur étant un tableau de décimaux, il n'est pas limité à 3 composantes, il peut en avoir autant qu'il en faut pour répondre au problème en main.

La propriété des vecteurs qui nous intéresse ici est qu'on peut soustraire des vecteurs entre eux. Pour cela, il suffit juste de soustraire les composantes des vecteurs une à une pour obtenir un nouveau vecteur. On écrit cette opération sous forme d'équation, comme ceci :

$$$$$\[\vec a-\vec b=\begin{pmatrix}a_x-b_x\\a_y-b_y\\a_z-b_z\end{pmatrix}\]$$$$$

Encore une fois, le vecteur n'a pas besoin d'être de dimension (=longueur) 3 pour que ça marche, même si, pour l'exemple, c'est ce que l'équation précédente montre. La soustraction tient pour n'importe quelle dimension du vecteur, on soustrait juste la première composante de l'un avec la première composante de l'autre, et ainsi de suite, après tout. Voici maintenant la formule de la distance euclidienne pour des vecteurs de dimension quelconque :

$$$$$\[d(a,b)=d(b,a)=\sqrt{\sum_{i=1}^d{(a_i-b_i)^2}}\]$$$$$

Si vous le savez pas, le grand signe en forme de E s'appelle "sigma", et, en mathématiques, il signifie simplement "prendre la somme sur un intervalle". L'intervalle est, dans notre cas, désigné par la lettre "i", qui va prendre toutes les valeurs de 1 à d, le nombre de dimensions du vecteur (le nombre de composantes qu'il a). Si vous n'avez pas bien compris l'opération, la voici sous forme de code :

float total = 0f;   //en maths, on calcule sur des réels
                    //donc, autant prendre des nombres réels dans le code
for (int i = start; i <= end; i++)
    total += i;

La formule précédente dit juste que la distance entre les points A et B est la même que l'on parte de A vers B ou de B vers A (c'est logique, la distance entre moi et mon lycée est de 650m que je m'y rende ou que j'en rentre), d'où l'équation d(a,b) = d(b, a), et que cette distance équivaut à la somme de la différence, au carré, entre chaque composante des vecteurs représentant les points A et B. Finalement, il y a une racine carrée, parce que jusque là, il y avait des carrés que l'on ajoutait entre eux. Pour trouver la vraie valeur de distance, on prend la racine carré.

La formule ci-dessus est directement (ou presque) donnée par le théorème de Pythagore, donc remerciez ce gentil monsieur !12

Qu'est-ce qu'on fait maintenant qu'on a la distance euclidienne ?

Maintenant qu'on sait comment la calculer, on va voir comment l'utiliser pour reconnaître un chiffre, évidemment !

Imaginez qu'on dispose d'une grille de pixels d'une taille, disons de 7x5, donc 7 pixels sur l'horizontale et 5 en verticale. On peut dessiner sur cette grille, mais uniquement en noir et blanc. Donc, à tout moment, un pixel peut ou être noir, ou être blanc, mais pas entre les deux, ni d'être d'une autre couleur. Disons qu'un pixel blanc vaut 0 et qu'un pixel noir vaut 1. On peut alors représenter chaque pixel comme étant une composante d'un vecteur, qui représentera la grille entière. Comme la grille a une taille de 7x5, il y aura 35 pixels, et donc 35 composantes dans le vecteur.

Notre algorithme pour la reconnaissance d'un chiffre, à l'aide de la distance euclidienne est donc tout donné.

Notre algorithme de reconnaissance

Vous l'avez peut-être déjà deviné, et le voici :

  1. Préparer des images de référence (que l'on va tenter de reconnaître).
    Cette étape revient à construire les vecteurs de ces images.

  2. Obtenir l'image dessinée par l'utilisateur (ce que l'utilisateur a écrit). Cela revient encore à obtenir le vecteur de son image.

  3. Comparer ce vecteur à tous les vecteurs dans notre base de référence, c'est-à-dire prendre sa distance euclidienne à tous les autres vecteurs.

  4. Le vecteur avec la distance la plus petite au vecteur de l'utilisateur est le chiffre que l'on a reconnu.

Cet algorithme est le plus simple pour faire de l'OCR, mais il a l'avantage d'être rapide à implémenter et à comprendre. Les résultats finaux ne sont cependant pas les meilleurs dans le monde de l'IA, c'est sûr et certain...

Implémentation

Nous allons maintenant l'implémenter pour avoir notre propre module de reconnaissance. Je vais utiliser le C# pour le code, mais j'ai fait attention à écrire les classes de façon claire pour pouvoir rendre l'écriture dans votre langage favori la plus simple possible.

Voyons de quoi nous avons besoin :

  • une classe pour représenter un vecteur

  • une classe pour représenter une image (grille de pixels)

  • une classe pour faire la reconnaissance

  • une classe pour obtenir l'entrée et présenter les résultats à l'utilisateur

Ces quatre points sont le strict minimum, mais j'ai rajouté de quoi permettre de rajouter de nouveaux caractères à reconnaître, et de quoi charger et enregistrer les images de référence, pour ne pas avoir à les re-rentrer dans le programme à chaque fois.

Je vais vous présenter chacune des classes, dans l'ordre de leur utilisation. Je ne vais pas commenter, puisque le code l'est déjà, cela dit.

MainForm (classe principale du programme)
using System;
using System.IO;
using System.Drawing;
using System.Windows.Forms;
using System.Collections.Generic;

namespace edocr
{
    //Représente la fenêtre du programme
    public partial class MainForm : Form
    {
        //L'entrée utilisateur
        ImageGrid inputGrid;
        //Les images de référence, avec leur nom
        Dictionary<string, ImageGrid> knowledge;

        //Si le bouton de la souris est appuyé
        bool mouseDown;
        //bmp et g sont pour le rendu
        Bitmap bmp;
        Graphics g;

        public MainForm()
        {
            //Crée la fenêtre
            InitializeComponent();

            //Initialise les éléments de rendu
            this.bmp = new Bitmap(Ozone.Width, Ozone.Height);
            this.g = Graphics.FromImage(this.bmp);

            //Crée une nouvelle grille vide pour l'entrée utilisateur
            CreateInputGrid();
            //Crée une liste d'images de référence vide
            this.knowledge = new Dictionary<string, ImageGrid>();

            //Définition des événements utiles
            Ozone.MouseUp += Ozone_MouseUp;
            Ozone.MouseDown += Ozone_MouseDown;
            Ozone.MouseMove += Ozone_MouseMove;

            bt_Clear.Click += bt_Clear_Click;
            bt_Recognize.Click += bt_Recognize_Click;
            bt_LearnCharacter.Click += bt_LearnCharacter_Click;

            bt_SaveData.Click += bt_SaveData_Click;
            bt_LoadData.Click += bt_LoadData_Click;

            lbx_LearnedItems.SelectedIndexChanged += lbx_LearnedItems_SelectedIndexChanged;
        }

        //Enregistre les images de référence dans un fichier texte
        private void bt_SaveData_Click(object sender, EventArgs e)
        {
            SaveFileDialog sfd = new SaveFileDialog();
            sfd.FileName = string.Empty;
            sfd.Title = "Save data to...";
            sfd.Filter = "Text data|*.txt";
            sfd.InitialDirectory = Environment.SpecialFolder.MyDocuments.ToString();

            if (sfd.ShowDialog() == DialogResult.OK)
            {
                string data = string.Empty;

                foreach (string itemName in knowledge.Keys)
                    data += knowledge [itemName].ToString() + Environment.NewLine;
                File.WriteAllText(sfd.FileName, data);
            }
        }

        //Charge une liste d'images de référence depuis un fichier texte formaté correctement
        private void bt_LoadData_Click(object sender, EventArgs e)
        {
            lbx_LearnedItems.Items.Clear();

            string file = GetDataFile();
            if (file == string.Empty)
                return;

            knowledge = new KnowledgeLoader().Load(file);

            foreach (string item in knowledge.Keys)
                lbx_LearnedItems.Items.Add(item);
        }

        //Utilisée pour le chargement. Retourne le chemin vers un
        //fichier sélectionné par l'utilisateur.
        private string GetDataFile()
        {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.FileName = string.Empty;
            ofd.InitialDirectory = Environment.SpecialFolder.MyDocuments.ToString();
            ofd.Filter = "Text data|*.txt";

            if (ofd.ShowDialog() == DialogResult.OK)
                return ofd.FileName;
            return string.Empty;
        }

        //Fait la reconnaissance de caractère
        private void bt_Recognize_Click(object sender, EventArgs e)
        {
            //Le nom du caractère reconnu, acquis à l'aide de l'algorithme de
            //la distance euclidienne
            string recognizedChara = DistanceMetric.EuclideanRecognition(inputGrid, knowledge);

            //Affichage du caractère reconnu ("R") et des distances vers les
            //autres images de références
            tb_Distances.Text = "R=" + recognizedChara + Environment.NewLine;
            foreach (var distance in DistanceMetric.Distances)
                tb_Distances.Text += distance + Environment.NewLine;
        }

        //Apprend un nouveau caractère. Tout ce que cette méthode fait
        //est de donner un nom entrée par l'utilisateur à l'image dessinée
        //par l'utilisateur et l'ajouter à la liste d'images de référence.
        private void bt_LearnCharacter_Click(object sender, EventArgs e)
        {
            if (tb_CharacterName.Text.Length == 0)
            {
                MessageBox.Show("Entrez un nom pour ce caractère !", "Euclidean distance OCR",
                    MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                return;
            }

            string charaName = tb_CharacterName.Text;
            ImageGrid charaGrid = inputGrid;

            knowledge.Add(charaName, charaGrid);

            lbx_LearnedItems.Items.Add(charaName);

            CreateInputGrid();
        }

        //Afficher la zone d'affichage et la grille utilisateur
        private void bt_Clear_Click(object sender, EventArgs e)
        {
            inputGrid.Clear();
            Render(inputGrid);
        }

        private void lbx_LearnedItems_SelectedIndexChanged(object sender, EventArgs e)
        {
            int index = lbx_LearnedItems.SelectedIndex;
            ImageGrid chara = knowledge [lbx_LearnedItems.Items [index] as string];

            Render(chara);
        }

        //La souris a été pressée
        private void Ozone_MouseDown(object sender, MouseEventArgs e)
        {
            mouseDown = true;
        }

        //La souris se déplace. Si elle est pressée, remplir les pixels correspondant.
        private void Ozone_MouseMove(object sender, MouseEventArgs e)
        {
            if (mouseDown)
            {
                inputGrid.IntersectMouse(e.Location);
                Render(inputGrid);
            }
        }

        //La pression sur la souris a été relâchée
        private void Ozone_MouseUp(object sender, MouseEventArgs e)
        {
            mouseDown = false;
        }

        //Crée une grille vide pour l'entrée utilisateur
        private void CreateInputGrid()
        {
            inputGrid = new ImageGrid(7, 5, Ozone.Size);
        }

        //Afficher une grille
        private void Render(ImageGrid grid)
        {
            g.Clear(Color.White);

            grid.Render(g);

            Ozone.Image = bmp;
            Ozone.Refresh();
        }
    }
}

DistanceMetric (l'algorithme de reconnaissance)

using System;
using System.Collections.Generic;

namespace edocr
{
    //L'algorithme de reconnaissance en lui-même.
    public static class DistanceMetric
    {
        //Une liste des distances et des éléments auxquels elles appartiennent.
        //Utilisée pour l'affichage.
        static List<string> distances = new List<string>();

        public static List<string> Distances { get { return distances; } }

        //Fait la reconnaissance à proprement parler depuis l'entrée
        //utilisateur et les images de référence.
        public static string EuclideanRecognition(ImageGrid input, 
            Dictionary<string, ImageGrid> knowledge)
        {
            distances.Clear();
            //Le nom du caractère le plus proche jusqu'à présent
            string closestGridName = string.Empty;
            //La distance du caractère le plus proche jusqu'à présent
            //A l'origine, la distance est infinie puisqu'on ne compare avec rien.
            float closestDistance = float.PositiveInfinity;

            //Le vecteur représentant l'image utilisateur
            VectorN inputVector = input.GetVector();

            //Pour chaque image de référence...
            foreach (string itemName in knowledge.Keys)
            {
                //... obtenir le vecteur représentant l'image de référence
                VectorN itemVector = knowledge [itemName].GetVector();
                //... calculer la distance entre l'entrée utilisateur et
                //    l'image de référence
                float distance = EuclideanDistance(inputVector, itemVector);

                //... si cette distance est la plus petite jusqu'à présent, le noter
                if (distance < closestDistance)
                {
                    closestDistance = distance;
                    closestGridName = itemName;
                }

                distances.Add(itemName + ":" + distance);
            }

            //Retourner le nom du caractère reconnu
            return closestGridName;
        }

        //Calcule la distance entre deux vecteurs.
        //C'est l'implémentation directe de la formule d(a,b) = sqrt(sum( (Ai - Bi)² ))
        //Lance une exception si les vecteurs sont de tailles différentes.
        private static float EuclideanDistance(VectorN a, VectorN b)
        {
            if (a.Dimension != b.Dimension)
                throw new ArgumentException("Vectors must be of the same size.");

            float sum = 0f;
            for (int i = 0; i < a.Dimension; i++)
                sum += (a [i] - b [i]) * (a [i] - b [i]);

            return (float)Math.Sqrt(sum);
        }
    }
}

ImageGrid (l'image)

using System;
using System.Drawing;
using System.Collections.Generic;

namespace edocr
{
    //Représente une grille de pixels, aka. une image en noir et blanc,
    //sans niveaux de gris.
    public class ImageGrid
    {
        //Dimensions de l'image
        int width, height;
        //Dimensions d'un pixel, utilisées pour l'affichage des pixels
        int itemWidth, itemHeight;

        //La grille de pixels en elle-même
        GridItem[,] grid;

        //Initialise une grille vide en connaissant ses dimensions
        //et les dimensions de la taille de dessin (utilisée pour déterminer
        //les dimensions des pixels)
        public ImageGrid(int width, int height, Size ozoneSize)
        {
            this.width = width;
            this.height = height;
            this.itemWidth = ozoneSize.Width / width;
            this.itemHeight = ozoneSize.Height / height;

            CreateGrid();
        }

        //Initialise une grille vide connaissant ses dimensions et celles
        //des pixels
        public ImageGrid(int width, int height, int itemWidth, int itemHeight)
        {
            this.width = width;
            this.height = height;
            this.itemWidth = itemWidth;
            this.itemHeight = itemHeight;

            CreateGrid();
        }

        //Donne une représentation littérale de l'image.
        //(Utilisée pour l'enregistrement de la grille)
        public override string ToString()
        {
            return "w=" + width + ";h=" + height + ";iw=" + itemWidth + ";ih=" + itemHeight
                + ";v=[" + GetVector().ToString();
        }

        //Vérifie si la souris est sur un quelconque pixel, et, si c'est le
        //cas, alors ce pixel est allumé (devient noir).
        public void IntersectMouse(Point mousePos)
        {
            Rectangle mouse = new Rectangle(mousePos.X, mousePos.Y, 1, 1);
            int x = mousePos.X / itemWidth;
            int y = mousePos.Y / itemHeight;

            if (grid [y, x].Intersects(mouse))
                grid [y, x].Fill();
            else
                grid [y, x].Empty();
        }

        //Donne la représentation vectorielle de la grille.
        //Donne le vecteur représentant l'image et utilisé pour
        //la reconnaissance avec la distance euclidienne.
        public VectorN GetVector()
        {
            VectorN vector = new VectorN();

            for (int x = 0; x < width; x++)
                for (int y = 0; y < height; y++)
                    //on construit le vecteur en mettant 1 ou 0 à la composante
                    //courante selon que le pixel soit blanc ou noir (Filled == true ou Filled == false)
                    vector.Add(grid [y, x].Filled ? 1f : 0f);

            return vector;
        }

        //Initialise une image depuis la connaissance de ses dimensions et
        //de celles des pixels, et du vecteur représentant l'image.
        public static ImageGrid FromVector(int width, int height, int itemWidth, int itemHeight,
            VectorN vector)
        {
            ImageGrid grid = new ImageGrid(width, height, itemWidth, itemHeight);

            for (int x = 0; x < width; x++)
                for (int y = 0; y < height; y++)
                {
                    //comme l'image est en 2D (tableau 2D) mais que le vecteur est un
                    //tableau 1D, on doit faire la correspondance entre les positions dans
                    //la grille 2D et le tableau 1D à l'aide de la formule
                    // i = x * hauteur + y où i l'index 1D et x et y la position sur la grille
                    if (vector [x * height + y] == 1f)
                        //évidemment, si le vecteur contenait un 1 à cet emplacement
                        //c'est que le pixel était noir, donc on le met en noir (Filled = true)
                        grid.grid [y, x].Fill();
                }

            return grid;
        }

        //Affiche la grille à l'écran.
        public void Render(Graphics g)
        {
            for (int x = 0; x < width; x++)
                for (int y = 0; y < height; y++)
                {
                    if (grid [y, x].Filled)
                        g.FillRectangle(Brushes.Black, grid [y, x].Hitbox);
                }
        }

        //Efface la grille : met tous les pixels en position vide (blanc).
        public void Clear()
        {
            for (int x = 0; x < width; x++)
                for (int y = 0; y < height; y++)
                    grid [y, x].Empty();
        }

        //Crée une nouvelle grille de pixels, vide.
        private void CreateGrid()
        {
            grid = new GridItem[height, width];

            for (int x = 0; x < width; x++)
                for (int y = 0; y < height; y++)
                    grid [y, x] = new GridItem(x * itemWidth, y * itemHeight, itemWidth, itemHeight);
        }
    }
}

KnowledgeLoader (chargeur d'images de référence)

using System;
using System.IO;
using System.Collections.Generic;

namespace edocr
{
    //Cette classe charge les images de référence depuis un fichier.
    //Elle prend un fichier contenant une suite de nombres (les vecteurs représentant les grilles)
    //et fabrique les grilles depuis ces vecteurs à l'aide de la classe ImageGrid.
    //La classe sert à charger des grilles (images) que l'on avait déjà créées,
    //pour ne pas avoir à les récréer.
    public class KnowledgeLoader
    {
        const int WIDTH = 0;
        const int HEIGHT = 1;
        const int ITEM_WIDTH = 2;
        const int ITEM_HEIGHT = 3;
        const int VECTOR = 4;

        public Dictionary<string, ImageGrid> Load(string file)
        {
            using (StreamReader fr = new StreamReader(File.OpenRead(file)))
            {
                int i = 0;
                string line = string.Empty;
                Dictionary<string, ImageGrid> knowledge = new Dictionary<string, ImageGrid>();

                while ((line = fr.ReadLine()) != null)
                    knowledge.Add((i++).ToString(), GetImageGrid(line));

                return knowledge;
            }
        }

        private ImageGrid GetImageGrid(string line)
        {
            string[] tokens = line.Split(new[]{ "w=", ";h=", ";iw=", ";ih=", ";v=[" }, 
                StringSplitOptions.RemoveEmptyEntries);
            int width = int.Parse(tokens [WIDTH]);
            int height = int.Parse(tokens [HEIGHT]);
            int itemWidth = int.Parse(tokens [ITEM_WIDTH]);
            int itemHeight = int.Parse(tokens [ITEM_HEIGHT]);

            string[] vectorComs = tokens [VECTOR].Split(',');
            VectorN vector = new VectorN();

            foreach (string com in vectorComs)
                vector.Add(float.Parse(com));

            return ImageGrid.FromVector(width, height, itemWidth, itemHeight, vector);
        }
    }
}

GridItem (un pixel)

using System;
using System.Drawing;

namespace edocr
{
    //Représente un pixel de l'image
    public class GridItem
    {
        //Le pixel peut être ou plein ou vide (noir ou blanc),
        //mais pas les deux, et il n'y a pas de niveaux d'intensité
        //entre les deux.
        bool filled;

        //Le rectangle représentant le pixel sur l'écran (zone de dessin).
        //Utilisé pour savoir quand la souris est sur le pixel,
        //et pour savoir où dessiner le pixel et comment le dessiner.
        Rectangle hitbox;

        public bool Filled { get { return filled;}}
        public Rectangle Hitbox { get { return hitbox; } }

        public GridItem(int x, int y, int w, int h)
        {
            this.filled = false;
            this.hitbox = new Rectangle(x, y, w, h);
        }

        public GridItem(Rectangle hitbox)
        {
            this.filled = false;
            this.hitbox = hitbox;
        }

        //Détermine si le pixel croise un objet rectangulaire.
        //Utilisée pour savoir si la souris (rapportée à un rectangle de 1x1)
        //est sur le pixel ou pas.
        public bool Intersects(Rectangle o)
        {
            return hitbox.IntersectsWith(o);
        }

        //Remplit le pixel (il devient noir).
        public void Fill()
        {
            filled = true;
        }

        //Vide le pixel (il devient blanc).
        public void Empty()
        {
            filled = false;
        }
    }
}

VectorN (un vecteur)

using System;
using System.Collections.Generic;

namespace edocr
{
    //Représente un vecteur de dimension N
    public class VectorN
    {
        //La liste des composantes du vecteur.
        List<float> components;

        //La dimension du vecteur, c'est-à-dire sa taille,
        //ou, autrement dit, le nombre de composantes qu'il a.
        public int Dimension { get { return components.Count; } }

        //Un indexeur permettant d'avoir accès aux composantes
        //directement en utilisant la syntaxe a[i] et non pas
        //a.components[i]
        public float this[int index]
        {
            get { return components [index]; }
        }

        //Constructeur vide, ne fait que créer une liste de float vide.
        public VectorN()
        {
            this.components = new List<float>();
        }

        //Constructeur initialisant les composantes selon une liste déjà donnée.
        //(Utilisé lors d'un chargement de donnée par le programme.)
        public VectorN(List<float> components)
        {
            this.components = components;
        }

        //Retourne une forme 'string' du vecteur.
        //(Utilisée pour sauvegarder le vecteur.)
        public override string ToString()
        {
            string str = components [0].ToString();
            for (int i = 1; i < components.Count; i++)
                str += "," + components [i];
            return str;
        }

        //Ajoute une composante au vecteur.
        //Ce design n'est sans doute pas le meilleur pour un vecteur,
        //puisque, mathématicalement parlant, ils ont une taille fixe,
        //mais du point de vue du programmeur, c'est le moyen le plus
        //facile de faire les choses, le plus comfortable.
        public void Add(float component)
        {
            this.components.Add(component);
        }
    }
}

MainForm.design.cs (le design de la fenêtre du programme)

// ------------------------------------------------------------------------------
//  <autogenerated>
//      This code was generated by a tool.
//      Mono Runtime Version: 2.0.50727.1433
// 
//      Changes to this file may cause incorrect behavior and will be lost if 
//      the code is regenerated.
//  </autogenerated>
// ------------------------------------------------------------------------------

namespace edocr
{
    
    #region Windows Form Designer generated code
    public partial class MainForm
    {
        private void InitializeComponent()
        {
            this.lbx_LearnedItems = new System.Windows.Forms.ListBox();
            this.bt_Recognize = new System.Windows.Forms.Button();
            this.bt_Clear = new System.Windows.Forms.Button();
            this.bt_LearnCharacter = new System.Windows.Forms.Button();
            this.Ozone = new System.Windows.Forms.PictureBox();
            this.tb_CharacterName = new System.Windows.Forms.TextBox();
            this.bt_SaveData = new System.Windows.Forms.Button();
            this.bt_LoadData = new System.Windows.Forms.Button();
            this.tb_Distances = new System.Windows.Forms.TextBox();
            // 
            // lbx_LearnedItems
            // 
            this.lbx_LearnedItems.Name = "lbx_LearnedItems";
            this.lbx_LearnedItems.Location = new System.Drawing.Point(24, 224);
            this.lbx_LearnedItems.Size = new System.Drawing.Size(144, 130);
            this.lbx_LearnedItems.BackColor = System.Drawing.SystemColors.Window;
            this.lbx_LearnedItems.TabIndex = 1;
            this.lbx_LearnedItems.ItemHeight = 14;
            // 
            // bt_Recognize
            // 
            this.bt_Recognize.Name = "bt_Recognize";
            this.bt_Recognize.Location = new System.Drawing.Point(256, 184);
            this.bt_Recognize.TabIndex = 2;
            this.bt_Recognize.ImeMode = System.Windows.Forms.ImeMode.Disable;
            this.bt_Recognize.Text = "Recognize";
            this.bt_Recognize.UseVisualStyleBackColor = true;
            // 
            // bt_Clear
            // 
            this.bt_Clear.Name = "bt_Clear";
            this.bt_Clear.Location = new System.Drawing.Point(352, 184);
            this.bt_Clear.TabIndex = 3;
            this.bt_Clear.ImeMode = System.Windows.Forms.ImeMode.Disable;
            this.bt_Clear.Text = "Clear";
            this.bt_Clear.UseVisualStyleBackColor = true;
            // 
            // bt_LearnCharacter
            // 
            this.bt_LearnCharacter.Name = "bt_LearnCharacter";
            this.bt_LearnCharacter.Location = new System.Drawing.Point(160, 184);
            this.bt_LearnCharacter.TabIndex = 4;
            this.bt_LearnCharacter.ImeMode = System.Windows.Forms.ImeMode.Disable;
            this.bt_LearnCharacter.Text = "Learn";
            this.bt_LearnCharacter.UseVisualStyleBackColor = true;
            // 
            // Ozone
            // 
            this.Ozone.Name = "Ozone";
            this.Ozone.Location = new System.Drawing.Point(32, 24);
            this.Ozone.Image = null;
            this.Ozone.TabIndex = 5;
            this.Ozone.Size = new System.Drawing.Size(400, 150);
            this.Ozone.BackColor = System.Drawing.SystemColors.ActiveCaptionText;
            this.Ozone.Text = "pictureBox1";
            // 
            // tb_CharacterName
            // 
            this.tb_CharacterName.Name = "tb_CharacterName";
            this.tb_CharacterName.ForeColor = System.Drawing.SystemColors.WindowText;
            this.tb_CharacterName.Cursor = System.Windows.Forms.Cursors.IBeam;
            this.tb_CharacterName.Location = new System.Drawing.Point(176, 320);
            this.tb_CharacterName.TabIndex = 6;
            this.tb_CharacterName.Size = new System.Drawing.Size(100, 24);
            this.tb_CharacterName.BackColor = System.Drawing.SystemColors.Window;
            this.tb_CharacterName.Text = "Char. learn";
            // 
            // bt_SaveData
            // 
            this.bt_SaveData.Name = "bt_SaveData";
            this.bt_SaveData.Location = new System.Drawing.Point(192, 248);
            this.bt_SaveData.TabIndex = 7;
            this.bt_SaveData.ImeMode = System.Windows.Forms.ImeMode.Disable;
            this.bt_SaveData.Text = "Save data";
            this.bt_SaveData.UseVisualStyleBackColor = true;
            // 
            // bt_LoadData
            // 
            this.bt_LoadData.Name = "bt_LoadData";
            this.bt_LoadData.Location = new System.Drawing.Point(192, 280);
            this.bt_LoadData.TabIndex = 8;
            this.bt_LoadData.ImeMode = System.Windows.Forms.ImeMode.Disable;
            this.bt_LoadData.Text = "Load data";
            this.bt_LoadData.UseVisualStyleBackColor = true;
            // 
            // tb_Distances
            // 
            this.tb_Distances.Name = "tb_Distances";
            this.tb_Distances.ForeColor = System.Drawing.SystemColors.WindowText;
            this.tb_Distances.ScrollBars = System.Windows.Forms.ScrollBars.Both;
            this.tb_Distances.Cursor = System.Windows.Forms.Cursors.IBeam;
            this.tb_Distances.Location = new System.Drawing.Point(288, 232);
            this.tb_Distances.TabIndex = 9;
            this.tb_Distances.Size = new System.Drawing.Size(152, 112);
            this.tb_Distances.ReadOnly = true;
            this.tb_Distances.Multiline = true;
            this.tb_Distances.Text = "Distances";
            // 
            // MainForm
            // 
            this.Name = "MainForm";
            this.ClientSize = new System.Drawing.Size(452, 379);
            this.Location = new System.Drawing.Point(26, 18);
            this.Controls.Add(this.lbx_LearnedItems);
            this.Controls.Add(this.bt_Recognize);
            this.Controls.Add(this.bt_Clear);
            this.Controls.Add(this.bt_LearnCharacter);
            this.Controls.Add(this.Ozone);
            this.Controls.Add(this.tb_CharacterName);
            this.Controls.Add(this.bt_SaveData);
            this.Controls.Add(this.bt_LoadData);
            this.Controls.Add(this.tb_Distances);
            this.Text = "Euclidean distance OCR";
        }
        private System.Windows.Forms.ListBox lbx_LearnedItems;
        private System.Windows.Forms.Button bt_Recognize;
        private System.Windows.Forms.Button bt_Clear;
        private System.Windows.Forms.Button bt_LearnCharacter;
        private System.Windows.Forms.PictureBox Ozone;
        private System.Windows.Forms.TextBox tb_CharacterName;
        private System.Windows.Forms.Button bt_SaveData;
        private System.Windows.Forms.Button bt_LoadData;
        private System.Windows.Forms.TextBox tb_Distances;
    }
    #endregion
}

Voilà, vous avez désormais un programme pouvant reconnaître un chiffre que vous dessinez, bravo !

Certes, l'algorithme est très simple, et il n'est pas optimal, mais il marche la plupart du temps, ce qui est déjà pas mal. Vous en verrez cependant bien vite les limites.

J'espère que vous avez apprécié la lecture de ce tutoriel, sur ce ! :)

Exemple de certificat de réussite
Exemple de certificat de réussite