# Journal de bord

## Mercredi 29 Mars 2023

----

Premier jour du travail de diplôme. Nous avons eu un briefing de mr Garcia et nous avons pu commencer à préparer le travail.

Nous avons eu les différents fichiers nescessaires à la bonne réalisation du projet et je me suis mis à faire les fichiers nescessaires

La première chose a été de faire ce mkdocs dans lequel j'ai mis un fichier yml plutôt standart qui risque de changer au fur et à mesure.

Voici le premier yml :

```
site_name: Documentation Diplome
theme:
        name: material
        palette:
        # Palette toggle for light mode
        - media: "(prefers-color-scheme: light)"
          scheme: default
          toggle:
          icon: material/brightness-7
          name: Switch to dark mode

        # Palette toggle for dark mode
        - media: "(prefers-color-scheme: dark)"
          scheme: slate
          toggle:
          icon: material/brightness-4
          name: Switch to light mode
markdown_extensions:
  - attr_list
  - md_in_html
plugins:
  - glightbox
  - with-pdf
```

Voici un example de à quoi ca ressemble en forme de site

!["Exemple mkdocs"](./Images/Screens/MkdocsExample.png)

Ensuite il m'a fallu faire une version plus à jour de mon cahier des charges car je n'y avait pas touché depuis novembre. J'ai envoyé un mail à mes enseignants pour qu'ils puissent y jeter un oeuil pour être sûr que je n'ai rien changé qui les dérangent.

Monsieur Jayr m'a demadé à l'occasion de lui faire un planning type Gantt alors je me suis mis à la tâche.

J'ai fait un planning prévisionnel et une légende les deux sont dispo dans le dossier planning de ce repertoire.

Ensuite je me suis mis à tout mettre sur git. A commencer par ce repertoire

Et c'est deja la fin de la journée ! Demain j'avance un peu sur la doc avec ce que je peux déja remplir et je finis de préparer ce dont j'ai besoin pour commencer à coder.

## Jeudi 30 Mars 2023

----

Aujourd'hui selon le planning je dois me charger des dernirers préparatifs pour commencer correctement. J'ai fait exprès de prenre du temps pour ca au début pour ne pas me créer de soucis plus loin pendant le travail.

Je vais envoyer par mail le planning que j'ai fait à mes suiveurs.

Ensuite je vais m'attaquer au squelette de la docmentation. Je vais essayer de remplir tout ce que je peux remplir dans un premier temps car cela tout ca de fait pour plus tard quitte à modifier quelques aspects au fur et à mesure.

J'ai aussi désactivé mkdocs with pdf par ce que les résultats ne sont vraiment pas ceux que j'attends et cela ralentis énormément le déploiment.

J'ai aussi rassemblé mes croquis pour le poster :

!["Croquis Poster 1"](./Images/Photos/photosCroquisPoster1jpg)
!["Croquis Poster 2"](./Images/Photos/photosCroquisPoster2jpg)

On peut voir que dans un premier temps j'ai tenté de faire un poster un peu plus stylisé et marketing. Cependant après avoir discuté avec Mr Garcia et différents profs dont un de l'HEPIA et ils m'ont indiqué que ce qui était attendu était moins du marketing qu'un diagramme de fonctionnement.

On peut voir sur les derniers posters que le coté technique ressort de plus en plus. Le but sera de faire une version encore plus technique ou on peut voir les différents fonctionnements de l'application avec les technologies utilisées.

Le défi cela va être de faire un joli poster qui soit en même temps vendeur et en même temps rempli techniquement.

Oh et j'ai eu un problème ou mon calvier et ma souris ne voulaient d'un coup plus fonctionner. Dans mon cas c'était un problème de power management des ports. J'ai eu le soucis sur mon pc fixe à la maison et sur mon pc portable également. En gros de ce que j'ai compris le soucis c'est que le pc croit que un port est trop solicité niveau puissance et du coup décide de couper l'alimentation du port USB.

J'ai pu règler le soucis en allant dans le device manager sous universal bus controller sous power management et en décochant la case qui indique que windows peut désactiver ce port.

Je ne conseille pas ce fix si vous avez des composants de mauvaise qualité car cela pourrait être une vraie alerte cependant le fait que mes composants sont plutôt haut de gamme et le fait que mon clavier et ma souris le fassent en même temps et que ils fonctionnaient très bien depuis plus de 4 ans me font penser que c'est juste une nouvelle mise a jour de windows qui est pénible.

Demain je vais pouvoir commencer à coder pour de bon.

## Vendredi 31/03/2023

Aujourd'hui on s'occupe de la PT2 qui est la programmation de la récupèration des informations des images.

Je vais tester IronOcr

Source : https://www.c-sharpcorner.com/article/ocr-using-tesseract-in-C-Sharp/
Doc : https://ironsoftware.com/csharp/ocr/docs/
Examples : https://ironsoftware.com/csharp/ocr/examples/simple-csharp-ocr-tesseract/

Avant d'utiliser la librairie je me demande si je dois utiliser un peu de post processing pour aider à la reconnaissance.

Je peux soit utiliser l'image cropée directement :

!["Image non traitée"](./Images/Screens/BlackToWhite0.png)

Soit avec un filtre pour passer en noir et blanc laxiste

!["Image traitée laxiste"](./Images/Screens/BlackToWhite1.png)

Soit avec un filtre pour passer en noir et blanc stricte

!["Image traitée stricte"](./Images/Screens/BlackToWhite2.png)

Il va falloir faire des tests avec tous les noms et les chiffres pour trouver le plus efficace.

Bon malheureusment Iron OCR semblait être une bonne alternative mais c'est une librairie privée qui demande une license pour être utilisée. Il va falloir trouver autre chose.

En utilisant la librairie "Tesseract" qui existe on peut faire de la reconnaissance de texte avec un code plutôt simple :

```Csharp
TesseractEngine engine = new TesseractEngine(tessDataFolder.FullName,"eng", EngineMode.Default);
            
var tessImage = Pix.LoadFromMemory(ImageToByte(sample));

Page page = engine.Process(tessImage);
string text = page.GetText();
```

Voici la methode ImageToByte :
https://stackoverflow.com/questions/7350679/convert-a-bitmap-into-a-byte-array

```Csharp
public static byte[] ImageToByte(Image img)
{
    using (var stream = new MemoryStream())
    {
        img.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
            return stream.ToArray();
        }
}
```

Voici le code pour traiter plusieurs textes sur une seule image :

```Csharp
Page page = engine.Process(tessImage);
            // Get the iterator for the page layout
            using (var iter = page.GetIterator())
            {
                // Loop over the elements of the page layout
                iter.Begin();
                do
                {
                    // Declare a Rect variable to hold the bounding box
                    Rect boundingBox;

                    // Get the bounding box for the current element
                    if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox))
                    {
                        g.DrawRectangle(Pens.Red,new Rectangle(boundingBox.X1,boundingBox.Y1,boundingBox.Width,boundingBox.Height));
                    }

                    // Get the text for the current element
                    var text = iter.GetText(PageIteratorLevel.Word);
                    tbxResult.Text += text.ToUpper() + Environment.NewLine;
                } while (iter.Next(PageIteratorLevel.Word));
            }
```

Etonnament, avec plus de texte, des noms qui étaient autrefois mal reconnus sont parfaitement interprêtés.

Par exemple voici un exemple de reconnaisance de texte sur tous les pilotes :

!["Screenshot de reconnaisance d'image complete"](./Images/Screens/TextRecognition1.png)

On voit que le nom Leclerc est mal reconnu. Mais voici ce que cela donne quand on prend une image qui ne contient que le nom Leclerc :

!["Screenshot de reconnaissance d'image cropée"](./Images/Screens/TextRecognition0.png)

On voit ici que le nom Leclerc est très bien reconnu.

Dans le premier exemple on peut voir que Tsunoda est reconnu comme "Reticin" ce qui n'est pas exactement pareil (lol)

Et quand on isole le nom Tsunoda dans une image seule :

!["Screenshot de reconnaissance de Tsunoda"](./Images/Screens/TextRecognition2.png)

Il le lit "RETLELYY" ce qui n'est toujours pas exactement ca...

Une meilleure résolution pourrait peut-être résoudre le problème en partie.

Jusqu'ici les images étaient en presque 720P ce qui donne ceci :

!["Tsunoda en 720P"](./Images/Screens/720PTsunoda.png)

Et j'ai lancé une récupèration d'images en 1080p pour récupèrer ceci :

!["Tsunoda en 1080P"](./Images/Screens/1080PTsunoda.png)

On peut voir une certaine différence tout de même.

Et quand on lance la reconnaissance :

!["Reconnaissance Tsunoda en 1080P"](./Images/Screens/TextRecognition3.png)

"Tsunoda n'est plus écrit "RETLELYY" mais "TSUNDDA" ce qui n'est pas parfait mais qui est déja beaucoup mieux.

J'ai essayé de mettre l'engine de Tesseract en mode "JPN" comme Tsunoda est un nom japonais mais sans succès j'ai le même résultat.

Comme la résolution est meilleure je me suis dit que peut être le filtre de passage en noir et blanc pourrait aider.

J'ai écrit cette petite methode pour convertir l'image en noir et blanc :

```Csharp
private static Bitmap ConvertToBlackAndWhite(Bitmap inputBmp)
        {
            const int BLACK_TO_WHITE_TRESHOLD = 200;
            Bitmap result = new Bitmap(inputBmp.Width, inputBmp.Height);

            for (int y = 0; y < inputBmp.Height; y++)
            {
                for (int x = 0; x < inputBmp.Width; x++)
                {
                    Color pixelColor = inputBmp.GetPixel(x,y);
                    if (pixelColor.R <= BLACK_TO_WHITE_TRESHOLD && pixelColor.G <= BLACK_TO_WHITE_TRESHOLD && pixelColor.B <= BLACK_TO_WHITE_TRESHOLD)
                    {
                        pixelColor = Color.FromArgb(0,0,0);
                    }
                    else
                    {
                        pixelColor = Color.FromArgb(255,255,255);
                    }
                    result.SetPixel(x,y,pixelColor);
                }
            }
            return result;
        }
```

Rien de bien dingue mais cela fonctionne et je peux jouer avec le BLACK_AND_WHITE_TRESHOLD pour changer son comportement.

J'ai dabord testé avec un treshold de 100 et le programme a réussi à me sortir Tsunoda en deux mots ce qui était déja très encourageant.

Et après avoir augmenté le Treshold... Tada :

!["Tsunoda 1080P avec filtre"](./Images/Screens/TextRecognition4.png)

Le programme arrive bien à reconnaitre TSUNODA. Je pense que cette tactique ne fonctionnait pas avant car la resolution était trop faible et l'aliasing se mêlait trop avec le texte pour être utilisable.

Cependant cette technique ne fonctionne pas sur tous les noms. Par example avec Leclerc :

!["Leclerc 1080P avec filtre"](./Images/Screens/TextRecognition5.png)

On récupère "Leeler'c" ce qui n'est pas bon du tout.

Mais en modulant le Treshold (ici à 150) On peut de nouveau voir Leclerc être reconnu correctement

!["Leclerc 1080P avec filtre 2"](./Images/Screens/TextRecognition6.png)

Je pense que pour avoir de bons résultats il va falloir faire un algo qui :

- Découpe l'image en autant de plus petites images pour avoir un mot par image.
- Teste voir si avec l'image originale un nom correspond à la liste de pilotes existant.
- Si cela ne marche pas, on applique le filtre en modulant le Treshold.
- Dans le cas ou on aurait pas un match parfait on fait un algo qui cherche le nom le plus proche qui existe dans la liste de noms donnés.

Seulement voila, il n'y a pas que des lettres que l'on veut récupèrer. On veut surtout pouvoir récupèrer les chiffres.

Pour les chiffres on va avoir des soucis également...

Si on essaie directement la même technique sans filtre on a des résultats comme celui ci :

!["Tentative de reconnaisance du timing"](./Images/Screens/TimingRecognition0.png)

La virgule a tendeance à se barrer ce qui est particulièrement problématique. Cependant comme les chiffres ont beaucoup moins de possibilitées que les lettres et qu'il n'y a pas de problème de langue on devrait pouvoir travailler à faire des règlage que l'on pourra ensuite utiliser.

Avec un Treshold de 165 on arrive presque à quelque chose d'intéressant :

!["Tentative 2 de reconnaissance du timing"](./Images/Screens/TimingRecognition1.png)

Le + n'est clairement pas compris mais ca n'est pas embêtant car c'est souvent redondant. On arrive cependant à isoler 3 et 259. Même si la virgule n'est pas comprise cela veut dire qu'il est tout de même possible de discriminer les secondes des milisecondes.

Maintenant avec un temps au tour :

!["Reconnaissance du timing au tour"](./Images/Screens/TimingRecognition2.png)

On arrive sans rien changer aux paramêtres à isoler minutes secondes et milisecondes.

Il semble que la reconnaissance de chiffre soit bien plus efficace que la reconnaissance de lettres. Il va falloir faire un test à plus grande échelle avec plus d'image pour se rendre compte de la precision.

Demain ce qui serait bien cela serait que je fasse un jeu d'images avec des valeurs connues et que je fasse une batterie de tests pour voir à quel point je peux faire confiance à la reconnaissance des chiffres.

Automatiser un système de test de la sorte me sera très utile dans le futur pour vérifier la non regression de ma reconnaissance de texte quand je tenterai d'y faire des changements.

Je suis toujours curieux cependant de voir comment le programme se débrouille avec les nombres de tours qui se trouvent dans les icones de pneus.

## Lundi 3 Avril

Aujourd'hui on va faire un programme qui permet de créer un dataset qui permette de tester le programme de reconnaissance.

Je pense que le meilleur moyen de faire serait un programme qui crée le dataset et qui ensuite peut tester différentes methodes de reconnaissance.

Par la même occasion je peux développer la technologie qui va permettre de découper une image en 20 lignes ce qui me servira ensuite pour la reconnaissance.

Je me rend compte que pour faire un programme de tests je dois déja avoir une idée de la structure de mon programme.

Pour le moment je réflechis à un système de "Zones" et de "Windows". L'idée serait que une Zone est juste une sous partie d'image qui peut encore être décomposé tandis que chaque Window contient une ou plusieurs informations à récupèrer.

J'ai essayé de découper l'image pour que cela soit plus clair :

!["Main zone"](./Images/Figma/WindowZoneExplanation1.png)

Ici on peut voir que l'image est découpée en plusieurs grandes zones. Dans un premier temps on ne s'occupe que de la première.

Ensuite :

!["Driver zone #1"](./Images/Figma/WindowZoneExplanation2.png)

On peut voir la que cette Main zone serait elle même décomposée en plusieurs plus petites zones.

Et ensuite chacunes de ces petites zones :

!["Driver windows"](./Images/Figma/WindowZoneExplanation3.png)

Sera décomposée en plusieurs windows qui elles sont des zones qui contiennent de l'information.

En gros on aurait trois types de zone :

- Les zones qui contiennent d'autres zones
- Les zones qui contiennent des Windows
- Les Windows

Cependant en y réflechissant on pourrait tout à fait avoir seulement des zones et des windows en faisant en sorte que les windows peuvent avoir une liste de windows et une liste de zones.

Une zone serait composée de :

- Une image de départ
- Un rectangle qui la positionne sur cette dernière
- Une liste de zones (potentiellement vide)
- Une liste de windows (potentiellement vide)
- Une methode qui permet de récupèrer une image de la zone
- Une methode qui permet de lancer la reconnaissance sur chaque window

Une window serait composée de :

- Une image de départ (cela peut être l'image cropée de la zone parente peu importe)
- Un rectangle qui la positionne sur cette dernière
- Une methode qui permet de récupérer un image de la window
- Une methode qui permet de lancer la reconnaisance sur l'image (Chaque type de zone doit l'implémenter)

Dans chaque window on peut imaginer que la methode qui fait la reconnaissance au lieu de retourner un objet qui peut contenir nimporte quel type d'information peut envoyer ce qu'elle vient de récupèrer dans une base de donnée ou un objet.

Par exemple une Zone de pilote pourrait très bien contenir un objet pilote et le donner à ses windows qui rempliraient ce même objet.

C'est une reflexion plus stockage que OCR mais c'est intéressant pour savoir ce que fait une window des données qu'elle récupère.

Dans un premier temps je pense que les windows vont simplement écrire dans un fichier ce qu'elles trouvent chacunes dans le format qu'elles veulent.

Pour comprendre pourquoi je me prend la tête il faut savoir que chaque window peut avoir accès à pleins d'informations différentes. On pourrait dire qu'elles retournent toutes une string sauf que si ca marche pour un temps au tour ou pour un nom de pilote, cela ne marche pas forcément pour un type de pneu ou un DRS ouver. Comme chaque window a plusieurs types de data elle devra elle même se charger de comment la traiter ET de la stocker.

Voila un diagramme qui résume comment je vois l'implémentation dans un premier temps :

!["Diagramme d'explications"](./Images/Figma/WindowZoneExplanationDiagram.png)

Voici comment se présente le squellette d'une Zone :

```Csharp
public class Zone
    {
        private Bitmap FullImage;
        private List<Zone> Zones;
        private List<Window> Windows;

        private Rectangle _bounds;
        public Rectangle Bounds { get => _bounds; private set => _bounds = value; }

        public Bitmap ZoneImage
        {
            get
            {
                Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height);
                Graphics g = Graphics.FromImage(sample);
                g.DrawImage(FullImage, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel);
                return sample;
            }
        }

        public Zone(Image fullImage, Rectangle bounds)
        {
            FullImage = (Bitmap)fullImage;
            Init(bounds);
        }
        public Zone(Bitmap fullImage, Rectangle bounds)
        {
            FullImage = fullImage;
            Init(bounds);
        }
        private void Init(Rectangle bounds)
        {
            Bounds = bounds;
            Zones = new List<Zone>();
            Windows = new List<Window>();
        }
        public void AddZone(Rectangle bounds)
        {
            if(Fits(bounds))    
                Zones.Add(new Zone(ZoneImage,bounds));
        }
        public void AddWindow(Rectangle bounds)
        {
            if (Fits(bounds))
                Windows.Add(new Window(ZoneImage,bounds));
        }
        private bool Fits(Rectangle inputRectangle)
        {
            if (inputRectangle.X + inputRectangle.Width > Bounds.Width || inputRectangle.Y + inputRectangle.Height > Bounds.Height || inputRectangle.X < 0 || inputRectangle.Y < 0)
            {
                return false;
            }
            else
            {
                return true;
            }
        }
    }
```

Le but est ensuite de créer différent types de Zones.

Par exemple la MainZone devra découper son contenu en 20 parties égales pour tenter de chopper les 20 pilotes. Il serait cool de trouver un moyen de calibrer automatiquement.

C'est peut-être possible de calibrer avec de la reconnaissance de texte, on peut essayer de lancer la reconnaissance et voir ou on trouve du texte avec un peu de chance cela pourrait donner les positions et avec ca on peut peut-être determiner des lignes.

Et voici le squelette d'une window générique

```Csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;

namespace OCR_tester
{
    public class Window
    {
        private Bitmap FullImage;

        private Rectangle _bounds;
        public Rectangle Bounds { get => _bounds; private set => _bounds = value; }

        public Bitmap WindowImage
        {
            get
            {
                Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height);
                Graphics g = Graphics.FromImage(sample);
                g.DrawImage(FullImage, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel);
                return sample;
            }
        }
        public Window(Bitmap fullImage, Rectangle bounds)
        {
            FullImage = fullImage;
            Bounds = bounds;
        }
        public virtual void RecoverInformations()
        {
            //Each Window type will have to implement its own way to recover the informations stored in the Window Image
        }
    }
}
```

Chaque Window pourra ainsi elle même implémenter la récupèration d'informations. La facon de les retourner/stocker est encore un peu floue.

Par exemple pour un temps au tour on peut imaginer que il fait une petite vérification dans l'objet pilote et dans le tableau des tours si il n'y a pas deja une valeur et si il n'y en a pas une alors il peut l'ajouter.

Maintenant je vais essayer de créer une Main window qui se calibre toute seule.

Alors après avoir bien galèré avec l'interface pour permettre au user de cliquer sur la form pour voir les zones qu'il crée, j'ai pu créer un zone qui fait les dimensions de MainZone et j'ai pu lancer la reconnaissance sur l'image et voir ou il trouve du texte :

!["MainZone avec carrés de texte"](./Images/Screens/AutoCalibration1.png)

Maintenant il faut que je nettoie la liste de rectangle pour exclure ceux qui sont trop grands pour être sur une seule ligne, ceux qui indiquent le nombre de tour en haut et ceux qui n'ont pas d'intérêts. On pourra ensuite isoler les lignes et créer une liste d'images.

Pour ce qui est de la ligne qui contient les "Gap interval last lap" et des chiffres sur les tours pour les pneus etc je vais juste demander à l'utilisateur de ne pas les prendre dans la screenshot. Comme ils contiennent des mots qui peuvent être utilisés plus loin dans les data je ne peux pas les blacklister et faire un système qui s'occupe de les enlever si ils existent selon le position y me prendrait trop de temps pour rien.

Après avoir filtré un peu les resultats et enlevé les zones beaucoup trop grandes, on se retrouve déja plus qu'avec ca :

!["MainZone avec de meilleurs carrés"](./Images/Screens/AutoCalibration2.png)

Comme on peut le voir, du côté gauche de l'image on a beaucoup de choses reconnues mais avec beaucoup de tailles différentes ce qui n'est pas idéal. Alors j'ajoute un filtre qui permet de ne selectionner que les data sur la droite.

!["MainZone avec de meilleurs carrés"](./Images/Screens/AutoCalibration3.png)

Maintenant il devrait être possible de faire un algorythme qui ne prend que un seul carré par ligne.

!["MainZone avec un seul carré par ligne"](./Images/Screens/AutoCalibration4.png)

Maintenant que on sait ou se trouve chaque ligne on peut faire un petit traitement et découper l'image en plusieurs windows.

Et voila :

!["Mainzone auto calibrée"](./Images/Screens/AutoCalibration5.png)

Maintenant le programme peut créer des zones pour chaque pilote

!["Images pilotes"](./Images/Screens/WindowsPilotes.png)

!["Zone d'un pilote"](./Images/Screens/driver2.png)

Maintenant il faut que j'implémente un système un peu similaire pour créer des windows.

Voici la methode que j'ai créé pour l'autocalibration :

```Csharp
public void AutoCalibrate()
        {
            List<Rectangle> detectedText = new List<Rectangle>();
            Zones = new List<Zone>();

            TesseractEngine engine = new TesseractEngine(Window.tessDataFolder.FullName, "eng", EngineMode.Default);
            Image image = ZoneImage;
            var tessImage = Pix.LoadFromMemory(Window.ImageToByte(image));

            Page page = engine.Process(tessImage);
            using (var iter = page.GetIterator())
            { 
                iter.Begin();
                do
                {
                    Rect boundingBox;
                    if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox))
                    {
                        //var text = iter.GetText(PageIteratorLevel.Word).ToUpper();
                        //We remove all the rectangles that are definitely too big
                        if (boundingBox.Height < image.Height / NUMBER_OF_DRIVERS) {
                            //Now we add a filter to only get the boxes in the right because they are much more reliable in size
                            if (boundingBox.X1 > image.Width / 2)
                            {
                                //Now we check if an other square box has been found roughly in the same y axis
                                bool match = false;
                                //The tolerance is roughly half the size that a window will be
                                int tolerance = (image.Height / NUMBER_OF_DRIVERS) / 2; 

                                foreach (Rectangle rect in detectedText)
                                {
                                    if (rect.Y > boundingBox.Y1 - tolerance && rect.Y < boundingBox.Y1 + tolerance)
                                    {
                                        //There already is a rectangle in this line
                                        match = true;
                                    }
                                }
                                //if nothing matched we can add it
                                if(!match)
                                    detectedText.Add(new Rectangle(boundingBox.X1, boundingBox.Y1, boundingBox.Width, boundingBox.Height));
                            }
                        }  
                    }
                } while (iter.Next(PageIteratorLevel.Word));
            }
            foreach (Rectangle Rectangle in detectedText)
            {
                Rectangle windowRectangle;
                Size windowSize = new Size(image.Width, image.Height / NUMBER_OF_DRIVERS);
                Point windowLocation = new Point(0, (Rectangle.Y + Rectangle.Height / 2) - windowSize.Height / 2);
                windowRectangle = new Rectangle(windowLocation, windowSize);

                Zones.Add(new Zone(ZoneImage, windowRectangle));
            }
        }
```

Ca peut paraitre pas énorme comme code mais pour tout mettre en place ca demande quand même pas mal de reflexion.

J'ai du clean un peu le code que j'avais fait pour permettre la selection de zones et ajouter la possibilité d'ajouter des windows sur une zone.

J'ai juste quelques difficultées à les ajouter correctement, j'ai un offset tout pourri qui se met tout le temps

!["Sainz coupé"](./Images/Screens/data0Sainz.png)
!["Perez coupé"](./Images/Screens/data0Perez.png)

Cela doit être un soucis lors de la detection de clic qui met un offset en trop. C'est vraiment pénible en tout cas.

Certes c'est moins fun de devoir manuellement indiquer ou sont les windows sur une ligne de pilote, mais je ne vois vraiment pas comment faire cela automatiquement. Le but c'est de faire une configuration qui puisse être sauvegardée comme ca pas besoin d'à chaques fois le refaire.

C'est bon ! J'avais juste oublié de changer le calcul d'offset entre le code de la zone et de la window. Note pour plus tard, il serait peut-être judicieux de faire quelque chose pour la vue, les windows et les Zones ont le même exact comportement pour la vue ce qui fait dupliquer du code.

Mais au moins maintenant ca fonctionne :

!["Ocr tester screenshot"](./Images/Screens/OCRTestingScreen.png)

Et le programme va directement créer un dossier par pilote avec toutes les images de chaque Data le concernant :

!["Dossier Perez"](./Images/Screens/PerezFolder.png);

Et c'est tout pour aujourd'hui je pense. Ce qui serait cool demain c'est que je puisse stocker d'une manière ou d'une autre ces fichiers de calibration et que je puisse les transfèrer vers le programme qui va s'occuper de décoder et commencer gentillement à décoder les différents types de data.

Note pour quand je ferai les tests. Je pense que la meilleure idée serait que je prenne pleins de photos du style et que je les mette dans un fichier CSV ou JSON avec leur contenu. Et ensuite je le fais passer en tests pour calculer la prescision de mon algo de décodage.

Pour le moment on est plutôt dans les clouts niveau planning.

## Mardi 4 Avril

Aujourd'hui je suis scensé plutôt bosser sur l'interpretation des données, mais une idée m'a taraudé l'esprit toute la nuit. Est-ce que je ne pourrais pas quand même essayer de décomposer la zone de pilote directment comme pour la Main zone.

Pour ce faire j'ai tenté de faire comme pour la main zone c'est à dire lancer la reconnaissance pour savoir ou étaient tous les champs de données mais malheureusement je ne pense pas que cela va être possible.

En effet non seulement ici les champs sont de tailles très variées, mais en plus la reconnaissance n'arrive pas à en récupèrer le même nombre sur chaque ligne ce qui risque d'être complexe à utiliser ensuite.

La preuve :

!["Tentative d'auto calibration"](./Images/Screens/AutoCalibration6.png);

Cependant tout n'est pas perdu ! Il y a peut-être un moyen qui serait mieux en tous points. Le soucis avec ce type de reconnaissance c'est qu'on utilise beaucoup de ressources inutiles. On peut peut-être hard coder la valeur des diviseurs et les utiliser pour créer des zones.

Ok alors visiblement c'est un problème car il semble y avoir d'autres pixels de cette couleur dans l'image (Qui l'aurait cru lol)

!["Tentative 2"](./Images/Screens/AutoCalibration7.png)

J'a tenté de réduire la tolérance mais le soucis c'est que c'est soit trop soit pas assez

Dernière tentative, j'ai essayé de prendre plusieurs pixels en hauteur pour chaque incrément de X et en faire la moyenne, et même comme ca, impossible de trouver de manière efficace les zones. Je pense que je vais donc revert tous mes changements pour revenir à la version ou on les choisissait manuellement.

Pas mal de temps perdu mais bon c'est comme ca ca arrive

Bon j'ai fait un revert mais j'ai ajouté une feature importante. Les zones font la largeur indiquée par l'utilisateur mais elles font la hauteur max comme ca toutes les window font la même hauteur et ca permet à l'utilisateur de ne pas forcément être ultra précis dans sa selection.

Ce qui nous donne :

!["Resultat final"](./Images/Screens/AutoCalibration8.png)

Maintenant je dirais que les deux prochaines choses à faire seraient de stocker ces zones dans un fichier JSON ou autre pour que la calibration puisse être envoyée directement dans le logiciel de reconnaissance et ensuite de faire une calibration sur des images qui font la taille qu'on aura pendant les Grands Prix. Pour le moment elles sont au format 16:10 qui est le format d'écrant de mon laptop.

Pour le stockage j'imagine un fichier qui donne des indications assez simples qui permettent de reconstruire le total des zones quand il est importé plutot que d'écrire les coordonnées en dur pour chacunes.

Chaque Grande zone va implémenter une methode qui s'occupe de mettre tous ses enfants dans un fichier.

```JSON
{
    "MainZone":{
        "x": 10,
        "y": 20,
        "width": 1450,
        "height": 1340,
        "DriverZone":{
            "x": 0,
            "y": 23,
            "height": 25,
            "Windows":[
                {
                    "DriverPositionWindow":{
                        "x": 0,
                        "y": 0,
                        "width": 35
                    }
                },
                {
                    "DriverPositionChangesWindow":{
                        "x": 0,
                        "y": 0,
                        "width":45
                    }
                }
            ]
        }
    }
}
```

C'est le résultat auquel j'aimerais arriver. Mais pour y arriver il faut encore que je crée les différents types de window.

Cela veut dire que je dois décider quelles informations je vais récupèrer de la page.

Par exemple je vais conserver la position du pilote mais au final les changements de positions sont difficiles à lire et sont redondants. Si je garde un historique des positions des pilotes je peux calculer moi même les changements.

Pareil pour gap avec la voiture devant. Je pense que je vais juste garder l'information des écarts absolus et ensuite je pourrai toujours calculer la différence entre les pilotes.

Ca peut paraître bête car cela rajoute du calcul mais en réalité le calcul de l'OCR est extrêmement gourmand alors il faut que j'évite le plus possible d'y faire recours. Il est bien plus rapide de calculer les écarts que d'essayer de reconnaitre le texte et le convertir en chiffre.

J'ai visiblement ajouté un bug dans mon code. Maintenant tous les pilotes ont la même image quand on les selectionne. Mais visiblement ca n'était pas le cas avant car j'avais pu prendres des images de chaque pilote.

J'ai passé 3 minutes à fixer un bug stupide j'ai un peu envie de brûler ma place de travail... Mais bon au moins maintenant cela fonctionne !

Toutes les images sont récupèrées et ont un format correct avec le bon nom :

!["Verstappen folder"](./Images/Screens/VerstappenFolder.png)

Avec un peu de code très moche j'ai pu créer un fichier JSON qui contient les différentes infos. Cependant en exportant TOUT on se retrouve avec un fichier de 1200 lignes ce qui n'est pas optimal.

Mais quand on regarde, il devrait être possible de faire un fichier qui ne contient que les infos d'un seul pilote car ensuite il y a simplement un offset à appliquer sur la zone et les windows.

Je vais donc pouvoir commencer enfin le logiciel de décodage qui prend en entrée un fichier JSON comme celui ci qui a été génèré avec le programme de calibration.

```Json
{
	"Main": {
		"x": 40,
		"y": 230,
		"width": 1845,
		"height": 719,
		"Zones": [
			{
				"DriverZone": {
					"x": 0,
					"y": 3,
					"width": 1845,
					"height": 35,
					"Windows": [
						{
							"Position": {
								"x": 2,
								"y": 0,
								"width": 32
							},
							"GapToLeader": {
								"x": 204,
								"y": 0,
								"width": 96
							},
							"LapTime": {
								"x": 413,
								"y": 0,
								"width": 105
							},
							"Drs": {
								"x": 526,
								"y": 0,
								"width": 81
							},
							etc...
						}
					]
				}
			}
		]
	}
}
```

Dans le futur il faudrait ajouter d'autres choses comme par exemple les différents pilotes présents sur le Grand Prix et ce genre d'infos.

Quoique je vais l'ajouter déja maintenant et plus tard je mettrai en place la feature acessible depuis l'interface. Mais le hardcoder maintenant me permet déja de mieux coder l'autre côté.

Ce programme n'est en aucun cas terminé et je vais devoir travailler encore un peu dessus pour qu'il soit utilisable correctement mais au moins il fonctionne à peu près.

Exemple du json avec les noms de pilotes:

```Json
{
	"Main": {
		"x": 37,
		"y": 238,
		"width": 1851,
		"height": 713,
		"Zones": [
			{
				"DriverZone": {
					"x": 0,
					"y": -5,
					"width": 1851,
					"height": 35
				}
			}
		]
	},
	"Drivers": [
		"Leclerc",
		"Verstappen",
        etc...
	]
}
```
Maintenant je vais m'attaquer au décodage.

Demain je dois finir le décodage du JSON et je dois commencer à implémenter la reconnaissance des textes. Voire même des pneus etc si j'y arrive.

## Mercredi 5 Avril

Bon la il faut vraiment que je finisse assez vite la lecture du JSON et la reconstruction des zones pour commencer la reconnaissance.

J'ai pris beaucoup de temps à faire le programme de calibration mais je pense que c'est essentiel de prendre ce temps maintenant. (BTW il faudra quand même retourner faire une plus jolie version par ce que la ca marche mais c'est tout)

Bon après pas mal de boulot je pense avoir réussi. Dans le nouveau programme on arrive à récupèrer les différentes zones :

!["JSON decode result"](./Images/Screens/JsonDecodeResult.png)

Un conseil de notre professeur M.Bonvin a été de créer des Releases de versions qui ne fonctionnent pas ou pas très bien. J'ai donc publié une première release de l'OCR_TEST qui fonctionne vite fait.

J'ai seulement un petit soucis, comme je recrée complêtement la structure des driver zones avec seulement la première, il y a un petit décalage car entre les zones il y avait un gap.

Ce qui fait que si la première zone est parfaitement centrée :

!["Centered driver"](./Images/Screens/CenteredDriver.png)

La vingtième ne l'est plus exactement :

!["Uncentered Driver"](./Images/Screens/UncenteredDriver.png)

Pour ca j'ai essayé de mettre un espacement arbitraire mais c'est complexe. Je vais plutôt tenter de faire une différence entre la taille de la zone complête et de la taille additionnée de toutes les fenêtre et diviser le resultat entre toutes les fenêtres.

Ca n'est pas parfait mais au moins maintenant les données ne touchent plus les bords de la fenêtre.

Et voila !

Maintenant avec le fichier de configuration en Json on arrive à récupèrer toutes les infos comme si elles avaient été envoyées directement depuis l'app de calibration mais sans le processing time !

!["Verstappen folder 2](./Images/Screens/VerstappenFolder2.png)

On peut donc ENFIN passer au décodage de ces FICHUES données.

Je vais pouvoir implémenter ce que j'ai fait dans le projet de test de décodage.

Grâce à mon découpage initial qui m'a pris du temps à implémenter on a enfin un truc qui marche même si je n'ai implémenté que la reconnaissance de noms.

!["Image reconnaissance propre"](./Images/Screens/ImageDecoding1.png)

Si on se rappelle du système de window et de zones dans le diagramme plus haut, c'est assez facile de comprendre comment je m'y suis pris.

En gros on des listes et des listes de listes de zones, c'est la partie un peu plus technique car il y a des zones qui peuvent contenir d'autres zones etc.

Je vais commencer par la reconnaissance de noms.

Voici le tableau de pilotes de 2023 

```JSON
"Drivers": [
		"Leclerc",
		"Verstappen",
		"Hamilton",
		"Alonso",
		"Russel",
		"Gasly",
		"Stroll",
		"Sainz",
		"Hulkenberg",
		"Norris",
		"Tsunoda",
		"Piastri",
		"Zhou",
		"Ocon",
		"Magnussen",
		"Perez",
		"Sargeant",
		"De Vries",
		"Bottas",
		"Albon"
	]
```

ET voici le tableau de pilotes de 2022 :

```Json
"Drivers": [
		"Leclerc",
		"Verstappen",
		"Sainz",
		"Perez",
		"Hamilton",
		"Russel",
		"Magnussen",
		"Gasly",
		"Ocon",
		"Alonso",
		"Tsunoda",
		"Bottas",
		"Zhou",
		"Albon",
		"Stroll",
		"Schumacher",
		"Hulkenberg",
		"Norris",
		"Latifi",
		"Ricciardo"
	]
```

Je les notes ici car J'ai souvent besoin de changer selon le dataset que j'utilise.

Dans le futur je ferai sûrement un grand dataset qui prend des pilotes de reserves et des pilotes juniors pour que dans le cas ou un pilote est remplacé dans l'année il n'y a pas besoin de tout recalibrer avec l'application.

Après une discussion avec M.Bonvin j'ai décidé de tester 3 valeurs de convertion en noir et blanc et si je ne trouve pas un match exact je prend le nom le plus proche.

Pour trouver la string la plus proche je pense que je vais utiliser quelque chose qui s'appelle la technique de Levenshtein. De ce que j'ai compris c'est un algorythme qui permet de donner une metric de différence entre deux strings.

Bon et évidemment il ne faut pas se tromper dans la liste des pilotes GENRE NE PAS OUBLIER QUE GEORGE RUSSELL COMPORTE DEUX WFNEWIEWV DE "L" A LA FIN DE SON NOM CE QUI POURRAIT ENGRANGER 2H DE DEBUGGING POUR RIEN ASK ME HOW I KNOW *joker laugh*

J'ai vraiment un soucis avec Tsunoda, Il a trop tendeance à le confondre avec "TSUNDDA" et pour des raisons obscures, quand j'applique l'algorythme de Levenshtein le plus proche n'est pas "Tsunoda" mais "Sainz" iniuvbwdiucbiubisc POURQUOI !!??!!

Je pense que cela demande moins de changements de lettres enfin bon c'est quand même pas idéal. Il va falloir que je trouve un moyen de le repondérer. C'est dommage par ce que cela marche super avec Alonso Verstappen et Albon.

J'ai un peu modifié la methode et j'ai fait en sorte d'envoyer tous les noms en majuscules en me disant que cela pourrait réduire le nombre de changements. Et ca a marché !! Cela va sûrement demander plus de tests pour être bien sûr que tout fonctionne nikel, cependant pour le moment ca marche parfaitement avec les pilotes de 2022.

Pour ce qui est de la reconnaissance de chiffres, j'ai déja fait une partie du boulot le premier jour alors je vais juste reprendre à partir de là.

Je récupère une string de ce type "1:35.123" le soucis c'est que les : se transforment parfois en . ou inversement mais bon ca devrait pas être trop dur à gèrer.

Il faut que je transforme cette string en nombre de milisecondes (Du moins je pense que c'est le meilleur moyen pour ensuite pouvoir facilement comparer et stocker l'information). 

Cela fait que 1:35:123 en milisecondes donne :

- 1 * 1000 * 60 => 60'000
- 35 * 1000 => 35'000
- 123 => 123

Total : 60'000+35'000+123 => 95'123ms

Et pour l'affichage :

- Minutes = ms / 60'000
- secondes = (ms - (minutes/60'000))/1000
- ms = ms - ((minutes 60'000) + (secondes * 1000))

Et on se retrouve avec 1:35:123

Maintenant après un peu de temps pour nettoyer la string etc on se retrouve avec un résultat comme le suivant :

```Text
Position : 0 
Gap to leader : 0:0:0 
Lap time : 2:15:123 
DRS : False
Tyre : Undefined laps with the tyre : 0 
Driver name : LECLERC 
Sector 1 : 0:31:323 
Sector 2 : 0:42:340 
Sector 3 : 0:0:0 
```

Evidemment pareil pour les autres pilotes
Et je me rend compte que j'ai encore tout cassé car le laptime ne devrait pas être 2:15 mais 1:35...

Voila après une heure de debugging et des ajouts pour nettoyer les chaines on se retrouve avec :

```Text
Position : 0 
Gap to leader : 0:0:0 
Lap time : 1:35:123 
DRS : False
Tyre : Undefined laps with the tyre : 0 
Driver name : LECLERC 
Sector 1 : 0:31:323 
Sector 2 : 0:42:340 
Sector 3 : 0:0:0 
```

Note: le traitement commence à devenir long, il serait peut-être intéressant d'utiliser un seul Tesseract Engine ou de voir ce qui prend autant de temps, on dépasse la seconde de traitement ce qui est un peu ma limite.

Après on peut toujours tester de rajouter du multicore processing mais c'est pour une autre fois.

Demain je m'occupe de règler les soucis que j'ai avec la prescision de ces temps au tour et j'éspère pouvoir m'occuper aussi de la position des pneus et du DRS. J'aimerais finir tout ca cette semaine.

## Jeudi 6 Avril

Une idée m'est passée par la tête pendant que je dormais, dans la liste des pilotes, quand ils sont à plus d'un tour de retard avec le leader (Ce qui arrive normalement dans presque tous les Grand Prix) on a pas des minutes mais une string qui montre "+1 Lap" ou "+2Laps" ce qui est évidemment un problème.
Je pense qu'une bonne facon d'envoyer l'info serait de retourner -1 -2etc... à la place des milisecondes, mais encore faut-il detecter le nombre de tours

Je devrais être en train de commencer la documentation de commment tout ce que j'ai fait fonctionne. Cependant je ne me vois pas faire ca tant que je n'ai pas au moins récupèré toutes les infos au moins un peu proprement. Cela veut dire que je commence officiellement à prendre du retard. (Sachant que si je finis tout aujourd'hui une journée de doc suffira largement le terme est un peu exagèré mais bon)

Bon pour la reconnaissance des temps c'est spécial...
Le filtre semble ne pas changer grand chose ce qui est problématique et ca n'est vraiment pas fiable.

Voici quelques expemples avec un treshold de 100:

!["11ZSD"](./Images/Screens/11ZSD.png)

Cette image est comprise comme "11ZSD"

![42340](./Images/Screens/42340.png)

Cette image est comprise comme "42340"

!["ZZAEB"](./Images/Screens/ZZAEB.png)

Et celle ci "ZZAEB"...

Ce qui... n'est pas bon du tout...

J'ai essayé de trouver un fichier d'entrainement spécifiquement fait pour les digits. J'ai essayé de blacklister les chars non voulus pour tenter d'obliger Tesseract à trouver des chiffres.

Avec la première option, les résultats ne sont pas meilleurs voire pires.
Avec la seconde option c'est déja pas mal mieux mais on perd complètement la possibilité de detecter les mots comme "LEADER" ou "LAP" et de toute facon ca n'est pas parfait.

Le soucis c'est que si je n'ai pas des données fiables c'est juste impossible de faire des calculs et de l'affichage correct...

Il faut absolument que je trouve une solution.

J'ai essayé d'utiliser de l'interpolation our augmenter la taille de l'image et ensuite appliquer mon filtre pour retirer le flou mais sans succès...

Pourtant la on se retrouve avec des images plutôt claires :

!["Clear1"](./Images/Screens/Clear44301.png)

Ici le programme trouve "44301"

!["Clear2"](./Images/Screens/ClearA5157.png)

Et ici "A5151"...

On a toujours les mêmes problèmes.

Bon je suis allé me renseigner sur l'OCR et je me suis dit que j'allais tenter de faire les choses proprement.

Je vais faire passer plusieurs étapes de postProcessing avant de donner l'image à Tesseract.

- GrayScale
- Tresholding
- InvertColors
- Scaling
- Dilatation

Ce qui donne : 

!["Original"](./Images/Screens/BeforeProcessing1.png)

!["Grayscale"](./Images/Screens/GrayScale.png)

!["InvertColors"](./Images/Screens/InvertColor.png);

!["Resize"](./Images/Screens/Scaling.png);

!["Dilatation"](./Images/Screens/Dilatation.png)

Ce qui ne change : *Roulement de tambour* RIEN kjd viuwvuirnvoirenbf

Tout ca pour rien...

C'EST BON !!!

Bon en fait au final le problème était une mauvaise configuration de Tesseract. Je vais devoir un peu nettoyer tout ca. Mais avec les changements de l'image on a des résultats BEAUCOUP plus précis et potentiellement utilisables.

La je vais devoir faire un serieux travail de nettoyage et simplification de mon code par ce que la c'est vraiment un chantier vu le nombre de choses que j'ai du essayer.

J'ai du aussi beaucoup modifier la gestion de l'image ce qui donne :

!["Clean"](./Images/Screen/../Screens/Clean.png)

Et la on a des résultats qui sont vraiment bons.

J'ai pu ajouter assez facilement la detection de position comme c'est simplement un chiffre.

On se retrouve maintenant avec ce genre de retours :

```Text
Position : 1 
Gap to leader : 8:33:51 
Lap time : 2:19:123 
DRS : False
Tyre : Undefined laps with the tyre : 0 
Driver name : LECLERC 
Sector 1 : 0:31:828 
Sector 2 : 0:42:940 
Sector 3 : 0:0:0 
Position : 2 
Gap to leader : 0:3:259 
Lap time : 23:12:392 
DRS : False
Tyre : Undefined laps with the tyre : 0 
Driver name : VERSTAPPEN 
Sector 1 : 0:38:119 
Sector 2 : 0:0:0 
Sector 3 : 0:0:0 
```

Il ne manque plus que l'implémentation de la reconnaissance du DRS et des Pneus

Et non... je viens de me rendre compte que mon programme a encore cassé car le tap time ne peut pas être 23 min lol.

J'ai un nouveau magnifique problème... Les points et les deux points sont interprêtés comme des chiffres ... Give me a F****** break...

J'ai du mal à comprendre pourquoi ils ne sont détectés comme tels que maintenant.

Bon alors il semblerait les temps au tour aie besoin d'un ordre très précis pour fonctionner.

- Grayscale
- InvertColors
- Tresholding
- Resize * 2
- Resize * 2

Et la on a des résultats un peu mieux.

Bon demain il faut absolument que je me charge de règler tous ces problèmes et que je commence la reconnaissance des pneus et de DRS par ce que je commence à être en retard.

## Vendredi 6 Avril 2023

Alors aujourd'hui c'est le dernier jour avant de commencer à être en retard pour de bon.

J'ai réussi à règler le problème des temps au tour, des gaps, et des secteurs. Dans le processus j'ai cassé la detection de position mais ca devrait pas être TROP compliqué.

Et voila ...
Après seulement plus de dix heures de galère, si on donne cette image au programme et le bon JSON le programme nous retourne :



```Text
Position : 1 
Gap to leader : 0:05:059 
Lap time : 1:39:123 
DRS : False
Tyre : Undefined laps with the tyre : 0 
Driver name : LECLERC 
Sector 1 : 0:31:828 
Sector 2 : 0:42:940 
Sector 3 : 0:00:000 
Position : 2 
Gap to leader : 0:03:259 
Lap time : 1:39:392 
DRS : False
Tyre : Undefined laps with the tyre : 0 
Driver name : VERSTAPPEN 
Sector 1 : 0:31:749 
Sector 2 : 0:00:000 
Sector 3 : 0:00:000 
```

Evidemment le GapToLeader est faux sur leclerc car il est leader mais bon ca je pourrai toujours Hardcoder que le premier a jamais de GapToLeader.

Bon j'ai eu beaucoup de soucis que je ne vais pas mentionner ici car ce sont simplement des soucis de logique de programmation pour trouver un DRS ouvert ou non.

Au final la technique que j'utilise et qui marche plutôt bien pour le DRS est que je prend la première image de DRS et je la déclare comme valeur étalon d'un DRS non actif, en effet dans 99% des cas le leader n'a pas de DRS (cela peut arriver alors il faudra donc juste verifier que les pilotes sont bien à moins de deux secondes les uns des autres pour confirmer).

Ensuite cette valeur étalon je la calcule en fonction du nombre de pixels verts dans l'image et si il y a plus de 30% de pixels verts en plus c'est que le DRS est activé ex:

Ceci est un DRS fermé:

!["Closed DRS"](./Images/Screens/DRSFalse.png)

Ceci est un DRS ouvert:

!["Open DRS"](./Images/Screens/DRSTrue.png)

Cela marche à peu près tout le temps mais dans le pire des cas on peut toujours verifier que les pilotes sont bien proches pour detecter les potentiels rares cas de faux positifs.

J'ai pu augmenter les performances en utilisant un seul engine pour tout le monde et en arrêtant d'utiliser GetPixel et SetPixel qui sont simplement des horreurs à utiliser. Mais elles ne sont pas encore bonnes

Le soucis avec la detection de pneus cependant, c'est qu'il n'est pas possible d'utiliser la reconnaissance pour savoir ou regarder la couleur car cela ne marcherait pas.
Je ne peux pas faire trop de post processing car je dois conserver la couleur
Je ne peux pas hardocder un endroit ou aller regarder car cela évolue tout le long du Grand Prix.

Bref c'est la galère. En y réflechissant je me suis dit qu'une bonne idée pourrait être de partir de la droite de la zone du pneu en regardant au milieu de la hauteur. Puis continuer vers la gauche jusqu'à ce que je rencontre une couleur différente. Je pourrai ensuite faire une zone un peu vers la gauche qui devrait contenir les infos du pneu et sur laquelle il sera possible de faire de le reconnaissance de couleur et de la reconnaissance de chiffres.

J'ai déterminé que le background n'était jamais plus clair que #505050 et que donc nimporte quelle couleur qui aurait plus que 50 dans un seul des channels serait considèrée comme une couleur cassant le background

Pour arriver à cette conclusion je me suis amusé un peu avec les couleurs pour jouer avec les limites de mon algorythme :

!["Color fun"](./Images/Screens/TestsCouleurs.png)

Et je crois que j'ai eu une bonne idée, avec une petite methode bien faite on arrive à de supers résultats :

```Csharp
private Rectangle FindTyreZone()
        {
            Bitmap bmp = WindowImage;
            int currentPosition = bmp.Width;
            int height = bmp.Height / 2;
            Color limitColor = Color.FromArgb(0x50,0x50,0x50);
            Color currentColor = Color.FromArgb(0,0,0);

            Size newWindowSize = new Size(bmp.Height,bmp.Height);

            while(currentColor.R <= limitColor.R && currentColor.G <= limitColor.G && currentColor.B <= limitColor.B && currentPosition > 0)
            {
                currentPosition--;
                currentColor = bmp.GetPixel(currentPosition,height);
            }

            //Its here to let the new window include a little bit of the right side
            int offset = Convert.ToInt32((float)newWindowSize.Width / 100f * 20f);
            int CorrectedX = currentPosition - (newWindowSize.Width - offset);
            if (CorrectedX <= 0)
                return new Rectangle(0,0,newWindowSize.Width,newWindowSize.Height);

            return new Rectangle(CorrectedX,0,newWindowSize.Width,newWindowSize.Height);
        }
```

!["Tyres"](./Images/Screens/TyresImages.png)

Maintenant cela devrait être beaucoup plus simple de trouver la couleur générale et le nombre de tours.

Donc ce que je fais c'est que je fais une reconnaissance de texte sur l'image réduite.

Si je trouve une lettre c'est facile Ca me donne le type de pneu et ca me dit que c'est le premier tour avec.

Si c'est un nombre alors je fais la moyenne de toutes les couleurs de l'image et je prend la couleur de pneu la plus proche.

Voici les différentes couleurs de pneus :

- SOFT : #FF0000
- MEDIUM : #f5bf00
- HARD : #d9d8d4
- INTER : #00a42e
- WET : #2760a6

!["Tyre colors"](./Images/Screens/TyreColors.png)

Les couleurs de pneus peuvent changer de temps à autres, par exemple cette règle de pneus est arrivée en 2019 et avant il y avait beaucoup plus de couleurs mais dans une volonté de rendre le sport plus facile à comprendre à la télé cela a été simplifié. Je ne pense pas que cela va changer dans les années qui viennent alors tout est hardcodé.

Je pense que j'ai des soucis avec la detection de texte et de couleur car ma zone est trop grande.

Alors bon j'écrit ces lignes apres des heures de tests.

Il semble que la principale difficulté avec ces pneus c'est que les chiffres ou lettres sont minuscules. Il est donc extrêmement difficile de faire une reconnaissance ne serait-ce qu'un peu fiable..

Je fais de mon mieux pour tenter de règler le soucis cependant c'est vraiment complexe.

Je commence à devenir fou, je tente tout et nimporte quoi pour permettre à mon algo de fonctionner et même quand je fais du post processing comme pas possible il me retourne toujours nimporte quoi...

!["5i t'inquiètes"](./Images/Screens/Perfect11But5.jpg)

Ici le programme va trouver '5i'...

En fait c'est complexe d'expliquer tout ce que je fais car je change tout en boucle en essayant et en ratant ce qui prend des heures.

Pour aujourd'hui j'abandonne je vais simplement rentrer chez moi et y réflechir cette nuit mais je ne vois pas comment mieux faire la...

C'est terrible par ce que je sens que je ne suis pas bien loin.

## Vacances

Bon je vais un peu laisser de côté la detection de chiffres pour me pencher un peu plus sur la détection de couleur. Par ce que techniquement si j'arrive à toujours parfaitement la detecter alors je pourrais me passer des chiffres car ils sont redondant si je construit un historique de pneus.

J'ai réussi à fix mon problème de mauvaise detection de couleur de pneus. Du moins je crois.

Seulement j'ai quand même un souci, les fenêtres de pneus avec une lettre n'ont pas assez de couleur pour être détectés. Je vais donc essayer de detecter les cinq lettres possibles et si il ne trouve pas alors je pourrai tenter de detecter les chiffres sans lettres ce qui devrait grandement aider.

Le but est encore une fois de réduire les possibilités de Tesseract. Je me rend de plus en plus compte que le plus important c'est de réduire le scope le plus possible. Moins il y a de mots et lettres et de chiffres possibles meilleure sera la reconnaissance.

Bon ca ne veut toujours pas marcher maintenant le 11 est interprêté comme trois I ou comme un M... J'en ai marre sans rire c'est vraiment pénible.

Alors j'écrit ces lignes deux jours plus tard et me rend compte avec horreur que toutes mes modifications sur ce journal de bord n'ont pas été auvegardée... yess..

Bon pour faire simple, j'ai réussi à rendre la detection de couleurs bien plus efficace en réduisant la taille de l'image et en ne prenant pas en compte les couleurs que l'on détecte comme étant partie intégrante du background.

Par exemple quand on a une image comme celle ci :

!["Avec background"](./Images/Screens/TyreWithBackground.png)

qui contient un background alors que ci dessous, on l'a enlevé.

!["Sans background"](./Images/Screens/TyreWithoutBackground.png)

La différence est ténue mais elle permet de grandement améliorer la prescision de la reconnaissance de couleurs.

Pour ce qui est du nombre de tours je me suis rendu compte que cela n'était déja pas très utile car avec l'historique on devrait pouvoir le déduire. Mais bon pour la forme je me suis dit que cela serait quand même une bonne idée de vérifier avec la reconnaissance. J'étais quasi certain que le soucis était le fait que l'on voie le contour du logo de pneu qui faisait que la reconnaissance avait du mal. Et j'avais raison ! En les enlevant (Ce qui n'a pas été simple) J'ai pu avoir des chiffres beaucoup plus proches de la réalité.

En même temps je ne vois pas bien comment j'aurais pu faire mieux : 

!["Super 11"](./Images/Screens/Recognised11.png)

Je suis quand même assez fier de voir que j'ai réussi à part de l'image que on peut voir un peu plus haut et automatiquement la transormer en celle ci-dessus.

J'ai donc pu retirer le round autour du chiffre et cela m'a permit de pouvoir dézoomer un peu et c'est avec ca que les lettres ont pu être mieux reconnues :

!["Super H"](./Images/Screens/RecognisedH.png)

!["Super M"](./Images/Screens/RecognisedM.png)

Maintenant je pense qu'il ne reste "plus qu'à" nettoyer un peu tout ce code qui traine de partout pour tout faire fonctionner et implémenter un peu de parrallel processing ainsi que de l'asynchrone pour ne pas bloquer le reste du programme.

Par ce qu'il faut savoir que en l'étât, le programme met 25 secondes à démarrer et consomme presque 2GB de Ram. Certes cela ne veut pas dire que la reconnaissance à elle seule prend 25 secondes car au démarrage il y a aussi la lecture du fichier de config et la création des window etc..

En réalité la partie strictement OCR prend dans les 12s si on en croit la fonction stopWatch de C#.

Et quand on change d'image la reconnaissance prend 9s.

Dans tout les cas c'est BEAUCOUP trop.

J'aurais eu comme objectif de faire une reconnaissance toutes les secondes. Je ne sais pas bien si cela va être possible mais en tout cas le but va être de s'en rapprocher.

Pour être plus exact et permettre une comparaison, voici les stats exactes

Avec un fichier d'images vide :

- Loading - 11.8s
- Splitting d'images - 90ms
- OCR - 12.5s

Avec un fichier d'images plein :

- Loading - 10.8s
- Splitting d'images - 80ms
- Ocr - 11.6s

En passant d'une image à l'autre : 

- Loading - NaN
- Splitting d'images - 50ms
- Ocr - 8.8s

Donc on peut voir que les deux endroits ou le programme prend le plus de temps c'est au premier démarrage quand il faut lire le fichier et setup les windows etc...

Et l'OCR qui prend un temps fou.

Ce qui est pratique c'est que les presque 2gb de ram sont utilisés que au lancement et ensuite l'application n'en utilise que quelques centaines de mb.

Le processeur lui tourne entre 10 et 20% ce qui ne va pas durer :)

Je vais m'occuper dabord du loading.

J'ai essayé d'utiliser un Parrallel.For au moment de la création des windows, le problème c'est que visiblement les objets windows sont beaucoup trops complexes et utilisent trop de ressources partagées pour être vraiment thread Safe. J'éspère que je n'aurais pas trop de soucis avec ca qu'en j'en viendrai à l'optimisation de l'OCR...

Ce qui me rend fou c'est que cette boucle toute nulle prend plus de dix secondes à s'executer et je ne comprend pas bien pourquoi.

```Csharp
for (int i = 0; i < NUMBER_OF_DRIVERS; i++)
                    {
                        Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset) /*- (i* (FirstZoneSize.Height / 32))*/);
                        Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize));
                        Bitmap zoneImg = newDriverZone.ZoneImage;

                        newDriverZone.AddWindow(new DriverPositionWindow(zoneImg, new Rectangle(driverPositionPosition, driverPositionArea)));
                        newDriverZone.AddWindow(new DriverGapToLeaderWindow(zoneImg, new Rectangle(driverGapToLeaderPosition, driverGapToLeaderArea)));
                        newDriverZone.AddWindow(new DriverLapTimeWindow(zoneImg, new Rectangle(driverLapTimePosition, driverLapTimeArea)));
                        newDriverZone.AddWindow(new DriverDrsWindow(zoneImg, new Rectangle(driverDrsPosition, driverDrsArea)));
                        newDriverZone.AddWindow(new DriverTyresWindow(zoneImg, new Rectangle(driverTyresPosition, driverTyresArea)));
                        newDriverZone.AddWindow(new DriverNameWindow(zoneImg, new Rectangle(driverNamePosition, driverNameArea)));
                        newDriverZone.AddWindow(new DriverSector1Window(zoneImg, new Rectangle(driverSector1Position, driverSector1Area)));
                        newDriverZone.AddWindow(new DriverSector2Window(zoneImg, new Rectangle(driverSector2Position, driverSector2Area)));
                        newDriverZone.AddWindow(new DriverSector3Window(zoneImg, new Rectangle(driverSector3Position, driverSector3Area)));

                        MainZone.AddZone(newDriverZone);
                    }
```

Alors que Zone.AddWindow c'est simplement :

```Csharp
public virtual void AddWindow(Window window)
        {
            Windows.Add(window);
        }
```

Et windows est simplement une liste. Donc ca ne peut pas être ca qui prend du temps.

Et les windows que je créé ont ca comme code : 

```Csharp
public DriverPositionWindow(Bitmap image, Rectangle bounds) : base(image, bounds)
        {
            Name = "Position";
        }
```

Sachant que le constructeur de base d'une Window c'est :

```Csharp
public Window(Bitmap image, Rectangle bounds)
        {
            Image = image;
            Bounds = bounds;

            Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, "eng", EngineMode.Default);
            Engine.DefaultPageSegMode = PageSegMode.SingleLine;
        }
```

Sachant que TesseractEngine est en statique et que donc il ne devrait... OHLLALALALALALALALALA je suis un imbécile...

J'ai juste à changer ce constructeur avec ca:

```Csharp
if (Engine == null)
            {
                Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, "eng", EngineMode.Default);
                Engine.DefaultPageSegMode = PageSegMode.SingleLine;
            }
```

ET le loading ne prend plus que 2-300 ms...

Bon c'est une très belle amélioration pour pas très chèr mais bon c'est un peu bête...

Bon je pense que 2-300ms c'est une durée correcte surtout que ca n'est appelé qu'une fois pour le lancement. On peut passer à la suite maintenant.

Alors il y a un grand soucis avec la parallellisation de l'OCR... Tesseract n'est pas par défaut une classe "Thread safe" ce qui veut dire que je ne peut utiliser de parallell.Foreach sur mes windows pour accèlèrer le traitement drastiquement.

Je pourrais par exemple avoir une instance de Tesseract par window sauf que cela fait 20 pilotes * 9 windows chacuns ce qui donne 180 instances ce qui n'est tout simplement pas raisonnable.

Je vais donc essayer de voir avec l'utilisation de methodes asynchrones qui me permettraient de faire un genre de flux tendu de reconnaissance. J'avoue que la je navigue un peu à vue, je me base sur différentes infos que je trouve sur des sites un peu perdus et sur chatGPT, j'espère que j'arriverai à trouver une solution car 10 secondes de reconnaissance c'est vraiment beaucoup trop.

Alors le soucis avec un Engine unique entre toutes les windows c'est qu'il n'est pas possible de process plusieurs images à la fois.

Je vais donc retirer l'engine unique pour voir si en créer un par window me permet de passer en multithreading.

La grande question sera : Est-ce que les ressources supplémentaires que vont prendre la création de tous ces engines va compenser entièrement le temps gagné avec la paralellisation.

Pour stocker les données dans un premier temps je vais créer un objet DriverData. Ce qu'il y a de pratique avec ca, c'est que je pourrais ajouter du code de vérification de certaines données directement dedans avant de les donner à la suite du programme.

Et on peut même imaginer une implémentation d'une liste de DriverData pour avoir l'historique.

Ce qui serait cool ca serait de grouper toutes ces data avec un numéro de tour. Placer ensuite la liste de Data dans une DB serait ainsi super simple. Mais il va falloir savoir quoi mettre, quelles infos sont redondantes et prendre en compte le fait que un tour affiché sur la page de la F1TV n'est accompli que par certains des premiers pilotes. D'autres pilotes peuvent être dans des tours précédents si ils ont du retard.

Il faudra réfléchir à cela quand je viendrai au modèle.

Bon pour y arriver j'ai du faire de gros changements et le résultat n'est peut-être pas aussi cool que ce que j'aurais voulut...

Voici un petit point sur les performances maintenant
J'ai également désactivé le dump d'images.
Pour le moment j'ai tout mis en commentaire mais cela pourrait être intéressant de faire en sorte de pouvoir l'activer en changeant une ou deux variables

Au démarrage :

- Loading - 113ms
- Splitting d'images - 14ms
- Ocr - 7s

En passant d'une image à l'autre :

- Loading - 113ms
- Splitting d'images - 13ms
- Ocr - 5s

Alors clairement les stats montrent qu'il y a eu un changement mesurable mais bon je pensais pouvoir en gagner un peu plus...

Je soupconne la création d'engines d'être à l'origine de ces performances presque décevantes.

Autre soucis, il semble que plus je change d'image plus la detection est lente et plus je consomme de RAM.

Il va falloir que je travaille encore un peu.

J'ai tenté de mettre un stopwatch sur une des créations d'engine Tesseract et le résultat me parait fou... Plus d'une seconde c'est dingue.

J'ai testé dans d'autres endroits du code et effectivement il semble que la création d'un engine prenne entre une et deux secondes ce qui est une ETERNITEE what !

Donc il faut optimiser tout ca.

Une idée serait de décomposer le threading mais cela me demanderait un gros refactor et je n'ai pas envie d'en refaire un la...

Sinon, une fois qu'ils sont créés ils ne prennent pas de temps du tout. Créer une fois tous les engines et ensuite les utiliser pourrait être une bonne idée. Cela prendrait longtemps au load mais ensuite les reconnaissances devraient être super rapides.

Ok alors ca c'est déja plus ce à quoi je m'attendais !
On est de nouveau à plus de 10s de loading time mais on est descendu à deux secondes par OCR. (Bon autre soucis, l'utilisation de la RAM est ridicule plus de 2gb mais ce qui m'inquiète c'est que j'ai l'impression qu'elle augmente plus on change d'image)

J'ai règlé (en partie) le soucis en obligeant le GC (Garbage Collector) à collecter après chaque detection. même après 50 detections l'utilisation de la ram se stabilize autour des 2GB.

Bon en paralellisant la création des Engines le soucis c'est que cela demande d'allouer beaucoup trop de mémoire d'un coup alors le programme se fige pendant genre cinq secondes avant de tout créer. Du coup même si la création est plus rapide, on se retrouve avec un temps total plus long... Je pense que l'on va devoir se contenter de ces dix secondes.

Bon la j'allais tenter de faire la documentation mais je viens de me rendre compte que la detection de temps au tour est pas vraiment encore idéale...

J'ai réussi à changer un petit peu le programme de reconnaissance pour rendre la reconnaissance un peu meilleure mais cela a drastiquement augmenté le temps requis pour décoder... On arrive à 3.5 secondes.

Je vais tenter de rajouter un peu de parralell processing sur les boucles de traitement voir si cela peut aider.

Alors effectivement cela aide pas mal, on arrive maintenant à faire une detection presque tout le temps en dessous de la seconde.

Et j'ai aussi du changer un peu le fonctionnement de la detection des Temps au tour.

Et voila je pense que je vais m'arrêter la pour la partie décodage. Je ne pense pas que je peux facilement faire mieux que ca et il faut que j'avance dans d'autres parties du projet.

Je vais pouvoir commencer à documenter un peu toute la partie OCR. Il faut que je prenne le temps de le faire bien car c'est la partie la plus intéressante du projet et ou je pense que j'aurai le plus essayé de choses qui vallent le coup dêtres racontées.

J'ai aussi passé pas mal de temps sur le poster du projet.
J'avais fait des croquis au crayon de ce à quoi je pensais, cependant après de longues discussions avec M.Garcia ils n'étaient pas forcément très bons car ils ne représentent pas assez bien le fonctionnement du projet et sont un peu trop marketings.

Du coup j'ai fait une première version au propre :

!["Poster V1"](./Images/Figma/PosterV1.png)

Mais je n'étais pas forcément content du résultat et il manquait des choses je trouve comme par exemple l'utilisation de Selenium.

J'ai donc repassé des heures à faire une seconde version :

!["Poster V2"](./Images/Figma/PosterV2.png)

La police d'écriture n'est pas encore la bonne mais cela va venir. Mais je préfère déja beaucoup cette version à la première.

Je ne sais pas encore si la version finale sera une version plus travaillée de ce poster ou complêtement autre chose mais pour l'instant je suis à peu près content de cette version.

Je le trouve un tout petit peu trop brouillon ou avec trop d'infos mais il m'a été de nombreuses fois reproché de ne pas assez montrer le fonctionnememt interne et je ne peux pas faire plus simple.

L'ajout des nombres pour compartimenter le projet ajoute de la structure mais je me demande si cela suffit.

Maintenant que je suis à peu près content de mon code pour l'OCR je vais commencer sa documentation. (Uniquement son fonctionnement interne pas comment s'en servir car cela va changer)

Bon j'ai créé u nouveau projet selenium mais même avec les bonnes libraries je n'arrivais pas à faire fonctionner firefox j'avais toujours une erreur "OpenQA.Selenium.WebDriverException: 'Cannot start the driver service on http://localhost:51481/'" et j'ai pu règler le problème en téléchargeant directement le gecko driver depuis le git https://github.com/mozilla/geckodriver/releases et utiliser le fichier directement dans le service :

```Csharp
var service = FirefoxDriverService.CreateDefaultService(AppDomain.CurrentDomain.BaseDirectory+@"geckodriver-v0.27.0-win32\geckodriver.exe");

FirefoxOptions options = new FirefoxOptions();

var driver = new FirefoxDriver(service,options);
```

Le seul problème c'est que du coup il faut tout le temps déplacer le fichier dans le dossier bin si je clone le projet. Il faudra faire un installeur dans la version finale qui s'occupe de tout je pense.