132 KiB
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
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 :
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 :
Soit avec un filtre pour passer en noir et blanc laxiste
Soit avec un filtre pour passer en noir et blanc stricte
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 :
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
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 :
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 :
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 :
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 :
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 :
Et j'ai lancé une récupèration d'images en 1080p pour récupèrer ceci :
On peut voir une certaine différence tout de même.
Et quand on lance la reconnaissance :
"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 :
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 :
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 :
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
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 :
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 :
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 :
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 :
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 :
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 :
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 :
Voici comment se présente le squellette d'une Zone :
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
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 :
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 :
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.
Maintenant il devrait être possible de faire un algorythme qui ne prend que un seul carré par ligne.
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 :
Maintenant le programme peut créer des zones pour chaque pilote
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 :
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
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 :
Et le programme va directement créer un dossier par pilote avec toutes les images de chaque Data le concernant :
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 :
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)
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 :
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.
{
"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 :
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.
{
"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:
{
"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 :
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 :
La vingtième ne l'est plus exactement :
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 !
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.
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
"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 :
"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 :
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 :
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:
Cette image est comprise comme "11ZSD"
Cette image est comprise comme "42340"
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 :
Ici le programme trouve "44301"
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 :
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 :
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 :
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 :
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é:
Ceci est un DRS ouvert:
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 :
Et je crois que j'ai eu une bonne idée, avec une petite methode bien faite on arrive à de supers résultats :
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);
}
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
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...
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 :
qui contient un background alors que ci dessous, on l'a enlevé.
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 :
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 :
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.
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 :
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 :
public DriverPositionWindow(Bitmap image, Rectangle bounds) : base(image, bounds)
{
Name = "Position";
}
Sachant que le constructeur de base d'une Window c'est :
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:
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 :
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 :
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 :
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.
Je me suis dit que j'allais garder la doc pour le retour des vacances quand j'aurai un bureau un clavier et un setup complet un peu propres.
Bon il va falloir que je parle de la récupération de cookie. J'ai déja pu travailler lors d'un poc sur la meilleure facon de prendres des screenshots de la F1TV :
- Avoir une page chrome ouverte avec le feed en plein écran et un programme qui prend des captures d'écrans.
- Avoir une caméra qui prend en photo l'écran au cas ou chrome et Firefox empêchent la prise de captures d'écrans.
- Récupèrer directement le feed en faisant du reverse engeneering de la plateforme.
- Simuler un chrome en background qui prenne des screenshot sans qu'on aie à le voir.
Dans toutes ces options, je dirais que la pire était celle de la caméra qui filme l'écran, mais à l'époque c'était encore envisageable comme solution de dernier recours. Le soucis de cette solution c'est l'horreur que serait la partie OCR avec une image de très mauvaise qualité.
Une autre option qui m'aurait vraiment embêté aurait été de devoir garder une page de Chrome ou Firefox ouverte quelque part sur un écran pour que le programme puisse prendres des captures d'écrans. C'est de loin l'option la plus simple et la plus logique mais elle possède pour moi de très gros points noirs :
- On ne peut pas certifier l'intégrité des données car l'utilisateur a le contrôle total sur le feed. Il peut mettre pause, avancer, reculer, tout casser sans faire exprès en ouvrant autre chose sur son ordi qui se mette pile devant. Bref c'est un peu bancale.
- Et surtout on bloque une partie non significative de l'écran de l'utilisateur avec des infos redondantes. Et je peux vous dire que quand je commente la F1 j'ai besoin de beaucoup d'informations et que chaque centimètre d'écran est cruciaé ! Alors avoir un écran complet bloqué est juste un point bloquant qui m'empêcherait d'utiliser l'app aussi bonne soit-elle dans ses prédictions.
Mais bon si aucune autre methode ne fonctionne ce qui est bien c'est que celle la est plutôt simple à mettre en place.
Ensuite reverse engeneer le feed serait l'option la plus classe, cependant c'est la plus complexe et la plus bancale au niveau légal haha. L'idée serait de récupèrer le lien vers le broadcast général et de comprendre comment il fonctionne pour le décoder nous même pendant un Grand Prix. Seuls soucis :
- Il n'est pas possible de faire des tests en dehors des periodes de Grand Prix (Et je rappelle que c'est des périodes ou je travaille en plus)
- Difficile de faire un système qui marche pareil pour les rediffusions et les lives. (En effet les liens des rediff sont beaucoup plus simple à récupèrer mais ne fonctionnent pas du tout pareil et pour tester l'app il est essentiel de pouvoir s'entrainer sur des anciens Grand Prix)
- Dernier GROS soucis, je ne sais tout simplement pas faire ca lol. Je ne sais pas comment faire. Peut-être que avec des profs qui m'aident et chat gpt ainsi qu'internet je pourrais potentiellement négocier un truc mais c'est hautement improbable et cela serait une perte de temps folle si je n'y arrive pas.
Dernière option que je trouve la plus séduisante. Simuler une instance de Chrome ou de Firefox (Le soucis avec chrome c'est qu'il implémente l'utilisation de DRM dans les vidéos qui fait qu'il est très difficile de passer outre la sécurité avec un bot) pour ensuite prendre des captures d'écrans automatiquement. Cette solutions offre pleins d'avantages :
- Pas de place prise sur l'écran
- L'intégrité des données est assurée car c'est le programme qui décide d'ou partir et de si il met pause ou non
- C'est une option complexe mais beaucoup moins que le reverse engeneering
- Elle permet de ne demander presque aucun input de la part de l'utilisateur.
Mais elle pose quelques problématiques :
- Comment se connecter automatiquement sans être detecté par un Bot et sans demander à l'utilisateur ses identifiants (Pour des raisons évidentes qui sont : QUI VA METTRE SES IDENTIFIANTS SUR UNE VIEILLE APP COMME LA MIENNE??)
- Comment faire en sorte que le programme prenne les meilleures captures dans la meilleure qualité et en plein écran.
Mais j'ai décidé de partir sur cette option.
Pour ce faire j'utilise Selenium. J'ai pu tester Puppetteer Sharp et même si dans un premier temps j'ai pu avancer asez vite, malheureusement il y a des bugs qui rendent son utilisation impossible dans notre contexte.
J'ai donc décidé de tout faire en utilisant un portage de Selenium dans mon programme.
Voici un exemple de code qui va ouvrir FireFox et qui va lancer un RickRoll
var service = FirefoxDriverService.CreateDefaultService(AppDomain.CurrentDomain.BaseDirectory+@"geckodriver-v0.27.0-win32\geckodriver.exe");
service.Host = "127.0.0.1";
service.Port = 5555;
FirefoxOptions options = new FirefoxOptions();
options.AddArgument("--disable-headless");
var driver = new FirefoxDriver(service,options);
driver.Navigate().GoToUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ&autoplay=1&mute=1");
Dans cet exemple on désactive le "Headless" pour qu'on puisse voir ce que fait l'app car sinon tout est invisible. Alors dans les faits la vidéo youtube ne se lance pas du tout car il y a des pubs et des prompts de cookies que l'on doit accepter etc... ce qui montre les différents challenges que l'on va devoir surmonter pour vraiment faire ce que l'on veut.
Mais un petit détail extrêmement important, la F1TV est un programme payant un peu comme netflix. Ce qui veut dire que pour accèder au contenu il faut être connecté. Sauf que une instance de firefox créé par Selenium est comme une page de naviguation privée, ce qui veut dire que si on va sur la page de la F1TV on est pas connectés.
Je pourrais tout à fait demander à l'utilisateur de me donner ses identifiants pour que j'aille ensuite automatiquement me connecter sauf que cela pose deux soucis:
- Personne ne voudra mettre ses identifiants sur mon programme
- La page de login de la F1TV a été protègée avec la meilleure technologie de detection de bots que je connaisse. Presque aucun site n'arrive à me detecter sauf eux ! Donc c'est tout simplement impossible d'utiliser cette technique.
Ensuite je me suis rappelé que ce que la page stocke pour me permettre de rester connecté ce sont des cookies. Et si je mets le bon cookies dans Selenium alors je serai connecté.
Dans un premier temps je voulais faire un système ou l'utilisateur irait prendre dans son chrome son cookie et le copie colle dans mon programme mais c'est immonde.
C'est alors que vient la partie récupèration de cookies !
Tous les cookies de chrome sont stockés dans une base de données SQLITE. On pourrait se dire Banco il suffit d'aller dedans et de retrouver tous les cookies et se connecter. Sauf que, pas bêtes, les équipes de chrome ont décidé que c'était une bonne idée d'encoder les cookies pour que tout le monde ne puisse pas venir y mettre son nez... En effet les cookies peuvent contenir des informations importantes.
Cela fait que pour utiliser ces cookies il faut pouvoir les décoder. Mon hypothèse a été que si ces cookies peuvent être lus par Chrome même hors connexion, c'est que la clé de décodage existe sur l'appareil et qu'il suffit de la trouver. ET C'EST LE CAS! Après pas mal de recherches j'ai pu voir que la clé de décodage existe bel et bien et qu'il suffit de la décoder en utilisant la librairie DPAPI pour la lire.
Avec cette clé on peut ensuite décoder les cookies et leurs valeurs ce qui veut dire qu'il est théoriquement possible d'automatiser le processus sans que l'utilisateur n'aie rien à faire.
J'ai décidé de faire la partie récupèration en python pour deux raison :
- Je n'arrivais pas à trouver une bonne implémentation de DPAPI en C# qui me permettait de décoder la clé.
- Il existe beaucoup plus de documentation en Python pour ce qui est de la cryptographie et donc si Chrome change de fonctionnement il sera beaucoup plus simple de changer cette partie en particulier sans avoir à recompiler le code C#.
J'ai donc avec l'aide d'internet et de ChatGPT créé ce script :
def get_master_key():
with open(
os.getenv("localappdata") + "\\Google\\Chrome\\User Data\\Local State", "r"
) as f:
local_state = f.read()
local_state = json.loads(local_state)
master_key = base64.b64decode(local_state["os_crypt"]["encrypted_key"])
master_key = master_key[5:] # removing DPAPI
master_key = win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1]
print("MASTER KEY :")
print(master_key)
print(len(master_key))
return master_key
def decrypt_payload(cipher, payload):
return cipher.decrypt(payload)
def generate_cipher(aes_key, iv):
return AES.new(aes_key, AES.MODE_GCM, iv)
def decrypt_password(buff, master_key):
try:
iv = buff[3:15]
payload = buff[15:]
cipher = generate_cipher(master_key, iv)
decrypted_pass = decrypt_payload(cipher, payload)
decrypted_pass = decrypted_pass[:-16].decode() # remove suffix bytes
return decrypted_pass
except Exception:
# print("Probably saved password from Chrome version older than v80\n")
# print(str(e))
return "Chrome < 80"
master_key = get_master_key()
cookies_path = Path(
os.getenv("localappdata") + "\\Google\\Chrome\\User Data\\Default\\Network\\Cookies"
)
if not cookies_path.exists():
raise ValueError("Cookies file not found")
with sqlite3.connect(cookies_path) as connection:
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
cursor.execute("SELECT * FROM cookies")
with open('cookies.csv', 'a', newline='') as csvfile:
fieldnames = ['host_key', 'name', 'value', 'path', 'expires_utc', 'is_secure', 'is_httponly']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
if csvfile.tell() == 0:
writer.writeheader()
for row in cursor.fetchall():
decrypted_value = decrypt_password(row["encrypted_value"], master_key)
writer.writerow({
'host_key': row["host_key"],
'name': row["name"],
'value': decrypted_value,
'path': row["path"],
'expires_utc': row["expires_utc"],
'is_secure': row["is_secure"],
'is_httponly': row["is_httponly"]
})
print("Finished CSV")
Ce programme va faire tout ce que j'ai expliqué et va ensuite stocker les résultats dans un CSV pour qu'il soit facile d'y accèder depuis le C#.
Alors oui cela pose certaines questions de sécurité. Car en effet je prend tous les cookies, les décode et les stocke. Ce qui veut dire que je pourrais tout à fait envoyer ces données quelque part, par exemple un compte Netflix, et me rincer.
Si je devais rendre le projet ouvert au public je pense qu'il faudra que cela soit mentionné clairement et que le projet soit open source pour que les utilisateurs puissent verifier que je ne fais pas ca.
Maintenant de l'autre côté j'ai juste à lire le CSV et le tour est joué !
(Trouver cette solution m'a pris une semaine de vacances à l'époque)
Bon j'ai réussi à faire le programme se connecter et naviguer etc..
Par contre quelque chose que j'ai voulu ajouter et qui m'a pris pas mal de temps c'est de faire en sorte de pouvoir selectionner la qualité.
Pour changer la qualité du feed il faut cliquer sur settings et ensuite prendre le menu deroulant et selectioner 1080p. Le soucis c'est le que la value du select est jamais la même.
Elle commence toujours pas "1080_" mais ensuite ca peut être "1080_45930285" ou "1080_56801" la suite est apparemment random.
J'ai donc du utiliser ce code pour le selectioner quand même :
IWebElement settingsButton = driver.FindElement(By.ClassName("bmpui-ui-settingstogglebutton"));
settingsButton.Click();
IWebElement selectElement = driver.FindElement(By.ClassName("bmpui-ui-videoqualityselectbox"));
SelectElement select = new SelectElement(selectElement);
IWebElement selectOption = selectElement.FindElement(By.CssSelector("option[value^='1080_']"));
selectOption.Click();
Sauf que pour que cela marche je dois avant cliquer sur le bouton des settings le problème c'est qu'il est invisible alors on doit le faire apparaitre.
J'ai tenté de le faire aparaitre en bougeant la souris, en cliquant à un endroit précis, impossible de le faire marcher correctement.
Puis j'ai eu l'idée de mettre pause en envoyant un appui sur la touche Espace et ca a permit de découvrir le bouton et permettre qu'on clique dessus.
Ca peut paraitre tout bête mais rien que ca, ca m'a pris un temps considérable.
Bon pour ce qui est du timecode de la vidéo. Je pense qu'il serait trop complexe de faire en sorte que selenium change le slider de progression de la vidéo. Alors j'ai fait quelques tests et apparemment, si on quitte la F1TV sur un timecode de la vidéo que on donne au programme, comme il récupère tous les cookies de la F1TV il commencera de la.
Donc si on veut utiliser le programme avec des Grand Prix ayant déja eu lieu, on peut le faire, seulement il faudra juste au préalable avoir choisit le bon timecode dans le page de la F1TV avant de le lancer.
Ce qui est intéressant c'est que la page de la F1TV ressemble à ca au départ :
Je pense qu'une bonne idée serait de dire au programme que c'est la grille de départ et ensuite dès qu'il détecte un secteur il sait que la course a commencé.
Lundi 24 Avril 2023
Aujourd'hui c'est jour de documentation.
J'ai pas mal travaillé pendant les vacances mais je n'ai pas encore pu faire de vraie documentation correcte du fonctionnement. Du coup je vais m'en charger aujourd'hui et peut-être un peu demain.
Ok normalement je ne devrais faire que de la documentation mais je ne peux pas passer à coté de ca... Le problème que j'ai avec les pneus ou parfois il détecte un H au lieu d'un '11' et ce genre de choses c'est à cause de ma methode "RemoveBG" Qui va retirer tous les pixels plus sombres que le background. Sauf que cela va aussi retirer des pixels dans le chiffre lui même et qui va donc defigurer les 11 :
J'ai réussi à les changer en :
Mais au final cela n'a pas augmenté la précision de la reconnaissance. Je pense que je vais donc devoir encore changer.
Je pense que une bonne facon de trouver serait dabord de trouver la couleur du pneu. Et si il n'y a pas assez de couleur alors c'est que le pneu contient une lettre. Le but est d'arrêter de chercher des lettres ou des chiffres. Comme ca les 11 arrêteront d'être pris pour des 'H'
En fait on peut faire encore plus simple que ca.
On peut simplement regarder la couleur dominante et determiner le pneu. En effet même si il y a une lettre sur fond noir pour décrire le pneu, mon methode de récupèration de la couleur dominante ommet les pixels trop noirs alors il est quand même possible de determiner le type de pneus.
Et tout simplement si il n'arrive pas à lire le chiffre c'est que c'est une lettre et que donc on est à 0 tours. Cela marche plutôt bien et cela simplifie pas mal le processing.
Voila, la je vais me remettre à la documentation sinon je vais encore prendre du retard.
Mardi 25 Avril 2023
Encore une fois j'ai pris du temps de doc pour changer des choses sur la partie OCR. Mais en même temps en documentant je vois des choses que j'ai soit mal fait soit que je pourrais faire mieux en changeant très peu de choses. J'éspère que les changement que j'ai fait vont aider au moins à la cohérence du code et un peu pour les performances.
Il semble que dans les conditions que j'ai testé le nombre de tour soit plutôt fiable mais je pense que je devrai faire un peu de travail en aval dans la récupération de ces données car je sens que cela va poser problème quelques fois. Je pense que en utilisant bien l'historique on peut potentiellement se passer de l'utilisation de ce chiffre pas toujours complêtement fiable.
Mais sinon aujourd'hui c'est encore une fois un gros jour de doc. J'essaie d'expliquer les différents procédés avant de les oublier. J'essaie aussi de donner un maximum d'exemples sous formes de photos intermédiaires mais ca me prend pas mal de temps car il faut que j'ajoute un peu partout dans le code des lignes pour sortir des images intermédiaires.
En plus de la documentation je me suis aussi beaucoup occupé de nettoyer mon code et je suis assez content par ce que même en ayant du rajouter des couches de complexité pour mieux reconnaitres les temps au tour j'arrive à un temps de processing parfois en dessous des 2 secondes ce que je trouve honorable.
Quand j'aurai finit de nettoyer tous mes fichiers je ferai une release sur gitea et ce sera la version que j'utiliserai quand je voudrai faire un merge avec les autres parties du projet.
J'ai beaucoup beaucoup bossé aujourd'hui et je sui bien mort. Faire autant de documentation et de nettoyage de code c'est pas forcément bon pour le cerveau je crois. J'ai besoin d'une sieste.
Demain je pense que je vais commencer à avancer sur la partie récupèration des images. Je sais que la je fais un peu passer les tests à la trappe mais déja j'en ai fait tout le long du développement de OCR_DECODE et il faut vraiment que j'avance, quitte à revenir dessus quand j'aurai merge les deux projets ensemble.
26 Avril 2023
Aujourd'hui je vais devoir m'occuper de la partie récupération des images. J'ai déja eu l'occasion d'avancer sur ce projet pendant mopn poc et mes vacances. Donc la le but ca va être de voir ce qui manque comme véritables features et ensuite je vais pouvoir m'occuper de la vue et de son intégration avec le décodage.
Ok donc maintenant que j'au un programme qui arrive à prendre des images depuis la F1TV correctement et en bonne résolution. Je pense qu'il est temps de passer à l'implémentation de la Forme que ca va prendre.
C'est important de se poser au moins cinq minutes la question de comment je prévois de faire car même si ca n'est pas la version finale, cette dernière prendra très fort inspiration du desing que je vais faire.
Dans cette form j'aurais besoin de :
- Pouvoir selectionner un Grand Prix en insèrant l'URL du feed.
- Pouvoir lancer la calibration si besoin
- Indiquer le titre et la date du Grand Prix
- Indiquer si le Grand Prix vient de commencer ou si il y a déja un certain nombre de tours lancés.
Et c'est à peu près tout en fait...
J'ai tellement poussé pour avoir un programme qui fait tout tout seul que il ne me faut pas grand chose de plus. Je pense que ce qui serait pas mal ca serait du coup d'utiliser ce temps pour bien implémenter la calibration qui elle aura besoin d'une UI un peu plus balèze.
On pourrait même imaginer que la calibration fasse partie intégrante des settings... Ca serait peut-être bien que quand l'application se lance on se retrouve sur la page principale d'affichage de données et qu'on puisse simplement cliquer sur la page options qui contient la page calibration et qui permet de rentrer les infos du Grand Prix.
Je pense que je vais faire ca.
Voici l'interface que j'ai développée pour regrouper tout ca :
La police le style le placement et les couleurs ne sont pas définitfs, cependant je pense que c'est un bon début. Le but maintenant va être de permettre de faire fonctionner la calibration et la récupèration d'images.
Si j'arrive à faire fonctionner ces deux choses sur un même projet avant la fin de la semaine cela serait super !
Bon J'ai pu avancer sur l'intègration de Selenium mais cela prend un peu de temps car je veux implémetner un moyen de pouvoir prendre une Screenshot à nimporte quel moment et pas juste en boucle.
Demain je finis de faire fonctionner ca et ensuite je commence le cablage du reste.
Jeudi 27 Avril 2023
C'est assez dur de faire l'importation car il y a des petites différences qui obligent à presque tout réécrire.
En fait le programme de calibration avait déja implémenté la fonction de Windows et de Zones mais il fonctionnait juste assez différemment pour qu'il faille tout refaire.
La je suis en train de perdre énormément de temps à cause d'un soucis de coordonnées. J'ai repris le code de la calibration pour detecter ou l'utilisateur a cliqué pour créer les zones. Cependant, je n'arrive pas à le faire fonctionner correctement. La zone est tout le temps décalée en haut et en bas mais pas de la même facon. En haut, la valeur Y est trop grande alors que en bas la valeur Y est trop petite... Je ne comprends pas bien pourquoi.
Si c'était un simple décalage cela ne serait pas compliqué à gèrer mais la...
J'ai un soucis également avec la résolution des screenshots que je récupère en full Headless.
Voici un exemple de résolution que j'arrive à récupèrer sans le headless :
Il y a clairement un soucis et le problème c'est que avec une résolution pareille, impossible de faire une reconnaissance correcte.
BON J?EN PEUX PLUS LA. Ca fait des heures que je bosse sur ce problème débile et impossible de trouver une solution. J'ai essayé cinq facons de forcer le browser headless a prendre une plus haute résolution aucune ne fonctionne je ne comprends pas.
A chaque fois que je me retrouve avec une résolution de 1366 x 768 Ou une variante de basse résolution du style. J'en peux plus je ne trouve aucune réponse sur internet ni même avec chatGPT.
Super... La seule chose que j'ai pu faire qui change quelque chose fait que les images font maintenant du 926x517... j'ai un peu envie de commentre un crime de guerre au plus vite.
Vendredi 28 Avril 2023
Une des solutions que je n'ai pas encore essayé est de changer ma version de GeckoDriver. Sauf que ca m'oblige à changer les versions de mes libraries ce qui est très pénible, je vais continuer le debugging dans le projet Selenium_clean.
Il faut savoir que la librairie de Selenium que j'utilise est bloquée en 0.27 ce qui fait que je ne peux utiliser qu'une version obsolète du Gecko Driver.
J'ai tenté de changer vers une version en 64 bits du GeckoDriver 0.27 mais pareil, je me retrouve toujours avec des images de M.
J'essaie toutes les solutions que je trouve sur internet aucune ne convient c'est infernal. J'essaie de changer la résolution DPI, j'essaie de changer les paramêtres par défaut des player de Firefox, j'essaie de changer la résolution pendant et au début de l'execution IMPOSSIBLE DE FAIRE MARCHER CETTE MERDE C'EST PAS POSSIBLE !!!
J'ai essayé avec chrome mais je ne peux pas l'utiliser car les DRM m'empêcheront de prendre des screenshot du flux vidéo.
J'ai essayé de faire tourner avec edge mais edge ne peut pas tourner en headless. JE VAIS DEVENIR FOUF FPWQOVMQEKOVNVIBDBJDAIVOBI.
ET MAINTENANT JE N'ARRIVE PLUS A FAIRE DE PROJET AVEC SELENIUM VOIWQNV(UEWQBVU)WEQN=OEJNIVIUWVBWUEV
ON CHERCHE A ME FAIRE PETER UN PLOMB C'EST PAS POSSIBLE GIWEGUWEQN
VOICI UN EXEMPLE DU CODE QUE JE DEMANDE A UN NOUVEAU PROJET AVEC EXACTEMENT LES MEMES LIBRARIES INSTALLEES :
// Create a new FirefoxDriver instance
IWebDriver driver = new FirefoxDriver();
// Navigate to the specified URL
driver.Navigate().GoToUrl("https://www.example.com");
// Do something with the driver (e.g., find elements or take screenshots)
// Quit the driver
driver.Quit();
Je ne demande que ca ET MEME CA CA NE VEUT PAS FONCTIONNER VOIWENB)IWUQENV
Oui je suis un peu énervé ca se voit? A bon?
Et maintenant NUGGET ne fonctionne plus... j'en peux plus la.
Je ne peux plus télécharger de librairie sur aucun de mes projets...
J'ai tenté de supprimer le fichier de config et redémarrer Visual Studio mais cela ne fait rien. J'ai aussi tenté de faire un 'nugget restore' toujours rien.
Bon apparemment je ne suis pas le seul qui ne peut pas accèder à Nuget donc bon c'est pas juste chez moi qu'il y a un soucis.
Mais même en mettant ma 4G pour me connecter, je n'arrive pas à accèder à certains sites y compris Nuget et je ne peux pas download de librairies...
Je ne comprends pas ce qui se passe et du coup je ne peux juste pas bosser... J'ai redémarré trois fois mon pc et visual studio, j'ai essayé de changer mes settings DNS etc... impossible de bosser.
Je crois que je n'aurais pas du me reveiller aujourd'hui.
Bon je vais tenter d'avancer sur mon poster en attendant que le réseau soit en meilleur état.
Lundi 1 Mai 2023
Bon je bosse depuis chez moi donc j'espère que Nuget va mieux fonctionner.
Après un weekend à réfléchir au sujet de cette resolution je me suis dit deux choses.
- La seule personne sur internet que j'ai vu avoir le meme soucis avait une résolution de 1920x1200 comme moi. Cela veut donc sûrement dire que le soucis vient de cette résolution de laptop comme moi.
- Si vraiment je n'arrive pas dans un premier temps à faire fonctionner le Headless correctement, je peux toujours laisser la page de côté et m'occuper du reste du programme. Certes ca serait vraiment infernal d'avoir à garder une page chrome ouvert en tous temps et en plus elle doit être en plein écran mais bon... Si il n'y a vraiment pas d'autres solutions malheureusement je serai bien obligé.
BON ! JE N'ARRIVE MEME PLUS A FAIRE UN PROJET QUI UTILISE SELENIUM ET QUI MARCHE JE VAIS FAIRE BRÛLER GENEVE. C'est pas possible serieux, je ne comprends pas j'essaie tout ce que je trouve et impossible de juste lancer firefox c'est du grand nimporte quoi. Je prend les même putain de librairies que sur les autres projets les mêmes versions, je prend le même exact code. Sur le nouveau projet impossible de le faire fonctionner. Je commence à croire que on essaie de me faire pêter un cable.
Du coup dans un élan de désespoir je vais tenter de passer sur une autre librairie qui avec un peu de chance marche et en plus me permettrais de prendre des foutues screenshot dans le bon format.
Les deux seules librairies qui pourraient potentiellement faire l'affaire sont les librairies :
- PhantomJS
- CefSharp
Je vais les tester et simplement prier pour qu'elles fonctionnent et que je puisse faire ce que je veux avec.
Alors pour le moment avec CEFSharp j'arrive à lancer une instance de chrome et prendre une screenshot avec ce code :
CefSettings settings = new CefSettings();
settings.CachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CefSharp\\Cache"); // Set cache path
settings.LogSeverity = LogSeverity.Disable; // Disable logging
Cef.Initialize(settings); // Initialize CEF
using (var browser = new ChromiumWebBrowser("www.google.com", new BrowserSettings())) // Launch Chromium in off-screen mode
{
browser.Load("https://www.example.com"); // Navigate to the test URL
browser.Size = new Size(1920, 1080); // Set the browser size to 1920x1080
browser.ScreenshotAsync().ContinueWith(task =>
{
var bitmap = task.Result;
bitmap.Save("screenshot.png", System.Drawing.Imaging.ImageFormat.Png); // Take a screenshot and save it as a PNG file
}).Wait();
}
Cef.Shutdown(); // Shutdown CEF
Avec ca il faut ces using :
using System;
using System.Drawing;
using System.IO;
using CefSharp;
using CefSharp.OffScreen;
C'est assez prometteur même si il faut encore beaucoup pour remplacer selenium.
Ah bah lol en fait non on peut pas utiliser cette librarie pour faire tourner firefox... J'EN AI MARRE J'AVAIS CHERCHE PRECISEMENT UNE LIB QUI MARCHE AVEC FIREFOX
Et phantomJS non plus ne fonctionne pas avec firefox... J'en ai marre.
Donc je vais plutôt partir sur la librairie GeckoFX qui semble pouvoir contrôler une instance de firefox. Mais j'avais justement pris un putain de projet C# et pas JS pour ne pas me taper ces problèmes de librairies...
Et si cette option ne fonctionne pas mon dernier espoir sera de directement intéragir avec le geckodriver.exe et la ca risque de pas être drôle.
JE NE COMPRENDS RIEN !!!!! Ca n'a aucun sens la doc est inexistante le seul lien qui pourrait amener sur une doc envoie sur la page principale de bitbucket. Tous les exemples de code que je trouve ne fonctionnent pas.
Je n'arrive à rien je commence à devenir fou. Tout ce travail pour rien c'est pas possible.
Même en essayant directement d'intéragir avec le process geckodriver.exe je ne peux pas arriver à mes fins. J'arrive à lancer le service et tout, mais je n'arrive pas à vraiment contrôler ce qu'il se passe donc impossible de venir prendre des screenshot.
Je ne sais tout simplement pas quoi faire ... Je suis bloqué. Je me suis cassé la tête à faire un truc qui marchait bien avec selenium et tout. Mais maintenant plus rien ne fonctionne du jour au lendemain et il n'y a simplement aucune alternative.
Je vais essayer de changer directement le projet Selenium_Clean mais bon ca va pas être drôle.
Ok alors j'ai tout repris depuis le début et je crois que j'ai enfin une solution.
Pour la trouver j'ai re-essayé toutes les techniques que j'avais tenté avant mais dans l'ordre et en les isolant à chaque fois.
Cela inclus :
Tenter de changer la densité de pixels. En effet je me suis dit que comme la résolution était plus basse le soucis était que le virtual screen avait simplement une DPI réduite.
profile.SetPreference("layout.css.devPixelsPerPx", "2.0");
J'ai aussi tenté de réduire à un seule le nombre de process de Firefox. J'ai pu lire sur internet que parfois cela pouvait influer sur les performances du renderer.
profile.SetPreference("dom.ipc.processCount", 1);
Ensuite j'ai tenté tout bêtement de rajouter dans la liste des arguments la taille voulue de l'écran.
options.AddArgument("--window-size=1920,1080");
Mais comme cela ne foncionnait pas, je me suis rabattu sur un script JS pour tenter de forcer la fenêtre à être plus grande.
js.ExecuteScript("window.resizeTo(1920, 1080);");
Comme cela n'a pas marché j'ai pu lire que cela pouvait être la taille intérieure qui devait être changée
js.ExecuteScript("window.innerWidth = 1920; window.innerHeight = 1080;");
Encore une fois sans succès. J'ai ensuite tenté d'utiliser trois autres versions du GeckoDriver, 0.27,0.26,0.25 et aucune ne m'aidait.
Mais en fait la seule chose qui a changé quoi que ce soit était la technique suivante :
Changer la window size en utilisant :
options.AddArgument("--width=1920");
options.AddArgument("--height=1200");
Ca ne marchait pas car j'utilisais une autre methode pour resize en même temps, qui elle ne marchait pas mais qui empêchait celle la de marcher. Ensuite le soucis que j'avais c'est que en mettant 1920-1080 je me retrouvais avec 1920-998 ou un truc du genre ce qui n'était pas normal alors je me disais que cette technique ne marchait pas non plus et je l'ai passée.
Alors tout n'est pas encore gagné, il faut que j'arrive à implémenter ca dans un plus gros projet et que la vidéo puisse être prise seule. Demain je m'occupe de ca.
Mardi 2 Mai 2023
Bon aujourd'hui je change le programme principal. Le soucis que j'ai c'est que en ajoutant ce système de resize, maintenant la page fait 100x100 et est grise. Il doit y avoir une technique que j'ai oublié de retirer ou un comportement un peu bizarrre.
Bon clairement je ne sais pas QUI DECIDE DE ME POURRIR LA VIE mais il est fort. J'ai télécharger EXACTEMENT les mêmes librairies que sur mon autre projet et j'utilise l'EXACT même geckodriver.exe mais dans le projet principal impossible de lui faire chier une image même avec l'EXACT même code. POURQUOI VOUS ME FAITES CA????=
La je ne comprend vraiment pas ce qui peut se passer pour que rien ne fonctionne alors que tout est pareil.
JE VIENS DE TOUT VERIFIER TOUT EST PAREIL JE NE COMPRENDS PAS.
Bon après avoir supprimé l'intégralité de ma classe Emulator cela semble marcher un peu mieux. Je ne vais pas m'étendre sur la castrophe niveau temps que cela représente. Si au moins j'arrive à faire fonctionner quelque chose je suis content.
Maintenant j'ai un soucis un peu spécial. Depuis que j'ai changé la résolution, il semble que le programme aie du mal à cliquer sur l'icone de settings.
En prenant des screenshots du moment ou l'erreur apparait, j'ai pu me rendre compte que en fait le stream est toujours en train de charger et c'est pour ca que on arrive pas à trouver le bouton :
Je pense que je n'ai le soucis que maintenant car le flux en 1080p se lance moins vite. Je vais essayer de voir si je peux detecter un élément d'HTML qui correspond au loading comme ca je peux attendre qu'il disparaisse. Sinon je peux aussi juste essayer de trouver le bouton en boucle pendant une dixaine de secondes.
Bon la j'essaie pendant genre plus de 50 secondes et ca ne marche toujours pas.
Il semblerait que au final le problème vienne du GP d'azerbidjan.
En effet, quand je teste un autre Grand Prix tout va bien.
ET MERDE !
J'ai réussi à avoir des images en 1080P mais dés que je passe l'image en plein écran c'est de nouveau du 1366X768
Avant de mettre en plein écran:
Après:
On peut voir sur l'image que l'option 1080P est effectivement bien selectionnée mais il doit y avoir un paramètre de Firefox qui s'occupe de la résolution d'un player vidéo. Il va juste falloir trouver ce paramêtre...
J'ai essayé d'utiliser :
Driver.Manage().Window.Size = new System.Drawing.Size(windowWidth, windowHeight);
Sans succès.
options.AddArgument("--start-maximized");
Pareil
Driver.Manage().Window.Maximize();
Toujours rien
profile.SetPreference("full-screen-api.ignore-widgets", true);
Nada
profile.SetPreference("media.hardware-video-decoding.enabled", true);
Toujours pas
J'ai vraiment cru que j'avais trouvé la solution en trouvant cette commande profile.SetPreference("full-screen-api.enabled", true); Mais non toujours pas...
Je commence à perdre patience.
C'EST BON.
Après littérallement 3h de debugging avec M.Bonvin (Que je remercie IMMENSEMENT) on a réussi à trouver au fin fond d'un thread github que la valeur était hard codée dans les variables d'environnement et que donc quoi que je fasse je n'aurais pas pu le changer.
En fait la seul moyen de tout règler a été de changer les variables d'environnement de ma machine:
MOZ_HEADLESS_WIDTH et MOZ_HEADLESS_HEIGHT.
Et ce qu'il y a de bien c'est que maintenant je peux mettre de la 4K et cela permet de faire un meilleur upscaling.
Recrutement Payerne Mai 2023
J'ai du faire mon recrutement à Payerne Mercredi et Jeudi. Si vous êtes curieux je peux vous dire que comme il n'y avait presque plus de places cet été je ferai Canonnier Lance mines. C'était assez frustrant d'avoir perdu deux jours de travail mais on va faire avec.
Vendredi 5 Mai 2023
Bon malgrés les courbatures il faut que je me mette au boulot un peu serieusement par ce que sinon ca va être compliqué de rattraper mon retard.
La dernière fois si je me souviens bien j'avais réussi à trouver un moyen de prendres des images en bonne résolution. Il faut maintenant que je commence à faire fonctionner la calibration et ce qui serait bien ca serait que je commence à ajouter la partie OCR au projet. Il faut que je me dépêche car Lundi je dois m'occuper du Poster.
OK j'ai compris le soucis que j'avais quand j'essayais de faire la calibration. J'avais mis l'image en ZOOM ce qui fait que si la hauteur n'était pas la bonne, l'image était recentrée ce qui fait que cela faussait totalement les résultats.
Quand on fait en sorte que l'image prenne toute la place, les coordonnées sont prises correctement.
Voici un exemple d'ou en est la partie calibration.
Normalement il me suffit d'implémenter les windows, et on devrait relativement facilement ajouter les pilotes.
Et voila. J'ai pu implémenter les windows et les pilotes. Et je peux aussi exporter des presets et les loader. Bon le loading est un peu beuggé au niveau de l'affichage mais il semble qu'il fonctionne bien quand je save les images.
Lundi je m'occupe du poster etc.. mais je pense que la suite va être l'implémentation de l'OCR.
Lundi 8 Mai 2023
Aujourd'hui c'est journée Poster.
Je pense que je ne vais pas finir la journée content car les limitations sont un peu trop présentes.
J'ai fait une version que Garcia pourrait accepter, c'est à dire en noir et blanc et avec un tout petit peu plus de détail.
Le truc c'est que en blanc je trouve que ca ne marche pas super. Et le concept d'avoir trois parties au projet qui se posent autour d'un circuit c'est peut-être pas la meilleure idée.
Je me suis dit que la bonne idée serait peut-être de prendre un autre circuit pour qu'il y aie bien trois parties :
Clairement ce poster doit faire partie des pires. C'est pas clair et ca part dans tous les sens. Je vais essayer avec un autre layout de circuit.
Je me suis ensuite dit que le circuit n'était peut être tout simplement pas une bonne idée. J'ai donc essayé de faire quelque chose de plus classique avec juste un peu de background pour qu'on puisse éviter le soucis de la page blanche derrière :
Puis je me suis dit que finalement le circuit me manquait. Alors j'ai décidé de combiner le background et le circuit ainsi que simplifier légèrement les diagrammes en retouchant un peu tout le reste on pouvait arriver à quelque chose de sympatique :
Je ne suis pas content à 100% mais bon je pense que je vais m'en satisfaire.
Pour donner une idée de la galère que c'est de créer un poster, voici ce à quoi ressemble mon espace de travail Figma :
Je ne suis pas un graphiste et ca se voit '^^.
Je pense que comme il me reste un peu de temps aujourd'hui, je vais faire un peu de documentation de la partie récupèration d'images. En effet, je pense que je n'aurai plus besoin de changer grand chose à ce niveau. Mais je ne ferai pas la partie analyse fonctionnelle car l'interface n'est clairement pas terminée.
En fait j'avais oublié mais j'ai eu un rendez vous médical du coup je n'ai pas eu trop le temps de faire la doc que je voulais. Mais au moins je pense avoir finit mon travail sur le poster et le abstract en Anglais qui sont les deux gros livrables à venir.
Mardi 9 Mai 2023
Bon je viens de me rendre compte que apparemment on doit rendre l'abstract anglais, le Poster, ET LE PROJET. Je pense que mes deux jours à l'armée m'ont fait perdre un peu la notion du temps car j'avais l'impression que l'evaluation intermédiaire 1 était il y a genre moins d'une semaine.
Donc aujourd'hui je ne vais pas trop avancer sur le code et vraiment me focus sur la documentation de la récupèration d'images. Je pense que je vais aussi ajouter la partie calibration à la documentation. Je pense que c'est important que je prenne le temps maintenant car sinon le prof aura l'impression que ca n'a pas trop avancé depuis la dernière fois.
Et puis je pense que la partie calibration et récupèration d'images ne va pas trop changer et la partie calibration encore moins.
La partie anglaise je fais la revoir un peu mais je l'avais déja faite pendant les premiers jours alors ca devrait aller.
Pour le rendu il nous était demandé de fournir un fichier PDF avec tout dedans avec une table des matières notre code source etc...
Pour ce faire j'ai du changer le mkdocs.yml et installer des packages.
Voici les changements ::
site_name: Documentation Track Trends
site_author: Rohmer Maxime
copyright: ©CFPTI Tech2
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
- pymdownx.highlight
plugins:
- glightbox
- search
- img2fig
- with-pdf:
cover_subtitle: Vroum Vroum
enabled_if_env: ENABLE_PDF_EXPORT
- annexes-integration:
annexes: # Required (at least 1)
- ConfigurationTool.cs: Code/ConfigurationTool.cs # An path to an annex with its title
- DriverGapToLeaderWindow.cs: Code/DriverGapToLeaderWindow.cs # An path to an annex with its title
- DriverPositionWindow.cs: Code/DriverPositionWindow.cs # An path to an annex with its title
- F1TVEmulator.cs: Code/F1TVEmulator.cs # An path to an annex with its title
- Program.cs: Code/Program.cs # An path to an annex with its title
- Window.cs: Code/Window.cs # An path to an annex with its title
- DriverData.cs: Code/DriverData.cs # An path to an annex with its title
- DriverLapTimeWindow.cs: Code/DriverLapTimeWindow.cs # An path to an annex with its title
- DriverSectorWindow.cs: Code/DriverSectorWindow.cs # An path to an annex with its title
- Form1.cs: Code/Form1.cs # An path to an annex with its title
- Reader.cs: Code/Reader.cs # An path to an annex with its title
- Zone.cs: Code/Zone.cs # An path to an annex with its title
- DriverDrsWindow.cs: Code/DriverDrsWindow.cs # An path to an annex with its title
- DriverNameWindow.cs: Code/DriverNameWindow.cs # An path to an annex with its title
- DriverTyresWindow.cs: Code/DriverTyresWindow.cs # An path to an annex with its title
- OcrImage.cs: Code/OcrImage.cs # An path to an annex with its title
- Settings.cs: Code/Settings.cs # An path to an annex with its title
- recoverCookiesCSV.py: Code/recoverCookiesCSV.py # An path to an annex with its title
Je remercie Monsieur Briard le sultan officiel de Mkdocs de la classe de m'avoir aidé pour cette partie et avoir créé un plugin qui me permet de mettre mon code source directement dans le pdf.
Bon au final j'ai quand même changé mon poster
Mais je suis trop attaché à l'ancien concept alors je vais plutôt utiliser ca :
Je pense que cette version est meilleure même si elle est encore plus en bordel par ce que le texte permet de se faire une meilleure idée de l'utilisé de chaque partie.
Mercredi 10 Mai 2023
Bon hier je n'ai pas eu le temps de finir la documentation de la recupèration d'images et de la calibration. Il faudra donc que je repasse un coup dessus en fin de semaine je pense.
Mais la j'aimerais avancer sur la mise en commun du projet, comme la configuration fonctionne plutôt pas mal je pense que je vais juste vite fait aller commenter les methodes qui ne le sont pas encore et ensuite je vais passer à l'implémentation de l'OCR.
Je suis presque certain que l'OCR va avoir besoin de plus de règlages mais bon on verra bien.
Je me rend compte en commentant que la methode de load serait plus efficace avec un tout petit peu plus d'infos de la part du JSON. J'aurais pu ajouter l'offset entre chaque Driver Zone pour eviter un lèger drift lors de la reconstruction. Mais bon rien de grave donc je pense que je vais le laisser comme ca pour le moment à moins que ca me pose soucis plus tard.
J'ai eu quelques soucis avec les images en 4K. Du coup j'ai descendu les variables d'environnement à 1920x1080
En fait il y a parfois un soucis un peu pénible avec l'OCR.
Parfois pour un temps comme ci dessous:
Le programme ne va pas bien comprendre les ponctuations et il va donner : 1115140
La il y a deux problèmes... Le 1:xx.xxx est compris comme 11xxxxx et le 4 s'est transformé en 1...
J'ai créé ce "petit" bout de code pour gèrer les fois ou les '.' et les ':' ont mal été interprêtés
if (rawNumbers.Count == 1)
{
//If this code is used it means that its bad ...
//The methods that comes are really not that great and are juste quick fixes
try
{
result = Convert.ToInt32(rawNumbers[0]);
switch (windowType)
{
case OcrImage.WindowType.Sector:
//The usual sector is in this form : 33.456
if (rawNumbers[0].Length == 6)
{
//The '.' has been understood like a number
result = 0;
result += Convert.ToInt32(rawNumbers[0][0] + rawNumbers[0][1]) * 1000;
result += Convert.ToInt32(rawNumbers[0][3] + rawNumbers[0][4] + rawNumbers[0][5]);
}
if (rawNumbers[0].Length == 5)
{
//The '.' has been overlooked
result = 0;
result += Convert.ToInt32(rawNumbers[0][0] + rawNumbers[0][1]) * 1000;
result += Convert.ToInt32(rawNumbers[0][2] + rawNumbers[0][3] + rawNumbers[0][4]);
}
break;
case OcrImage.WindowType.LapTime:
//The usual Lap time is in this form : 1:45:345
if (rawNumbers[0].Length == 6)
{
//The '.' and ':' have been overlooked
//I Know Im skipping the cases where there are more than 9 minuts but it happens so rarely that... we dont care
result = 0;
result += Convert.ToInt32(rawNumbers[0][0]) * 60000;
result += Convert.ToInt32(rawNumbers[0][1] + rawNumbers[0][2]) * 1000;
result += Convert.ToInt32(rawNumbers[0][3] + rawNumbers[0][4] + rawNumbers[0][5]);
}
if (rawNumbers[0].Length == 7)
{
//There is two possibilities
//Either 1:45.140 has been interpreted as 1145.10 or 1:451140. We will assume its the first one
result = 0;
result += Convert.ToInt32(rawNumbers[0][0]) * 60000;
result += Convert.ToInt32(rawNumbers[0][2] + rawNumbers[0][3]) * 1000;
result += Convert.ToInt32(rawNumbers[0][4] + rawNumbers[0][5] + rawNumbers[0][6]);
}
break;
case OcrImage.WindowType.Gap:
//The usual Gap is in this form : + 34.567
if (rawNumbers[0].Length == 5)
{
//The '.' has been overlooked
result += Convert.ToInt32(rawNumbers[0][0] + rawNumbers[0][1]) * 1000;
result += Convert.ToInt32(rawNumbers[0][2] + rawNumbers[0][3] + rawNumbers[0][4]);
}
break;
}
if (rawNumbers[0].Length > 6)
{
//The number definitely has been interpreted wrong
}
}
catch
{
//It can be because the input is empty or because its the LEADER bracket
result = 0;
}
}
else
{
//Auuuugh
result = 0;
}
ConfigFile = "./Presets/Clean_2023.json";
string gpUrl = "https://f1tv.formula1.com/detail/1000006688/2023-azerbaijan-grand-prix?action=play";
Bon je n'arrive pas à faire fonctionner l'OCR sans tout faire crash à chaque fois. Je vais abandonner le travail de la journée pour revenir au point initial... C'est très frustrant mais bon je ne vois pas comment faire mieux. Rien ne marche alors qu'avant ca marchant super sur le projet OCR normal.
Va savoir pourquoi même comme ca, impossible de faire marcher l'OCR. Il y a un soucis au niveau de l'ASYNC qui me fait crash tout le temps en me disant qu'un objet est deja en train d'être utilisé. Ca marchait nikel dans mes premières version je ne vois pas pourquoi ca pête maintenant.
Je pense que je vois à peu près le soucis.
public virtual async Task<DriverData> Decode(List<string> driverList)
{
int sectorCount = 0;
DriverData result = new DriverData();
Parallel.ForEach(Windows, async w =>
{
// A switch would be prettier but I dont think its supported in this C# version
if (w is DriverNameWindow)
result.Name = (string)await (w as DriverNameWindow).DecodePng(driverList);
if (w is DriverDrsWindow)
result.DRS = (bool)await (w as DriverDrsWindow).DecodePng();
if (w is DriverGapToLeaderWindow)
result.GapToLeader = (int)await (w as DriverGapToLeaderWindow).DecodePng();
if (w is DriverLapTimeWindow)
result.LapTime = (int)await (w as DriverLapTimeWindow).DecodePng();
if (w is DriverPositionWindow)
result.Position = (int)await (w as DriverPositionWindow).DecodePng();
if (w is DriverSectorWindow)
{
sectorCount++;
if (sectorCount == 1)
result.Sector1 = (int)await (w as DriverSectorWindow).DecodePng();
if (sectorCount == 2)
result.Sector2 = (int)await (w as DriverSectorWindow).DecodePng();
if (sectorCount == 3)
result.Sector3 = (int)await (w as DriverSectorWindow).DecodePng();
}
if (w is DriverTyresWindow)
result.CurrentTyre = (Tyre)await (w as DriverTyresWindow).DecodePng();
});
return result;
}
Ca c'est ma methode de decoding de chaque Driver Zone. Le message d'erreur me parle d'une windowImage quand il dit qu'un objet est déja utilisé. Ma conjecture c'est que en essayant de faire toutes les windows en même temps. Elles veulent parfois accèder à l'image principale en même temps. Ce qui evidemment pose problème. Je pense que le fix le plus simple serait de faire le traitement sans le parallele quitte à exporter ce fonctionnement sur chaque zone en elle même pour ne pas perdre trop de performances.
Ok je crois que je vois ou est le soucis. En fait dans cette version du programme c'est toujours la première image qui était juste tout le temps prise et dans la première image on a une partie des chiffres qui est bloquée par l'UI de la fenêtre... lol...
EN FAIT J'avais un soucis dans ma gestion des chiffres mal faits. Visiblement parfois quand je ne prenais pas en compte un :, un LapTime etait compris comme un Gap to leader ou un Secteur
Bon j'en ai tellement marre... Je n'arrive tout simplement PAS à faire fonctionner l'OCR ca crash tout le temps j'en peux plus.
J'ai tenté de règler les problèmes de mauvaises detections de secteurs et temps au tour qui font crasher l'app :
if (rawNumbers.Count == 2)
{
//ss:ms
result = (Convert.ToInt32(rawNumbers[0]) * 1000) + Convert.ToInt32(rawNumbers[1]);
if (result > (60000 + 999))
{
if (windowType == OcrImage.WindowType.LapTime)
{
result = 0;
result += Convert.ToInt32(rawNumbers[0][0]) * 60000;
result += Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString()) * 1000;
result += Convert.ToInt32(rawNumbers[1]);
}
if (windowType == OcrImage.WindowType.Sector)
{
int seconds = 0;
if (rawNumbers[0].Length == 3)
{
//We have one char that we need to delete
//For no apparent reason im going to delete the first
seconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
}
else
{
seconds = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString());
}
int ms = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
result = seconds * 1000 + ms;
}
}
}
Mais toujours impossible de faire fonctionner cette M**** C'est juste infernal. Je pense que je vais encore tout retirer et remplacer par ce que j'ai dans mon projet OCR original. Donc c'est une journée de perdue complêtement... C'est extrêmement frustrant.
Après des heures de debug j'ai enfin réussi à faire fonctionner le programme de temps en temps. Mais j'ai toujours le soucis que l'image ne veut pas changer alors que je fais tout pour et que l'OCR est nulle à chier du coup...
Jeudi 11 Mai 2023
Bon après une bonne nuit de sommeil je vais reprendre les choses depuis le début.
J'ai deux soucis :
- L'OCR pue du derche
- L'Image que l'on décode ne change pas
Pour la première partie j'ai ma petite théorie. Je pense que comme je donne des images 4K alors que le feed est en 1080P, il y a déja un genre d'interpolation qui est faite. Je pense donc qu'il faut que j'adapte mon engine pour qu'il fonctionne avec cette résolution.
Je me suis demandé si ca n'était pas mieux de prendre en compte les deux résolutions pour les pc un peu moins balèzes et j'ai décidé de n'en avoir rien a faire. On verra dans le futur si c'est une feature que je voudrais ajouter mais c'est en dehors du scope du diplôme je pense.
Pour la seconde partie, je pense qu'il faut que j'aille voir du côte de OCR_Decode et de OCR Tester pour voir comment je faisais. Je dois forcément oublier un truc.
Bon ca commence mal, quand je vais voir dans le projet OCR_Decode, le changement d'image est exactement le même et il fonctionne alors que de mon côté ca n'est pas le cas.
Alors deux choses. Je me rend compte que le changement d'images n'a AUCUN effet sur la detection de texte, et seconde chose, le décalage est trop grand entre les windows. Des que le soucis d'image est règlé il va falloir que je change drastiquement ma facon de stocker la config en JSON. Il faut que je conserve les écarts.
Sinon regardez ce que ca donne quand on arrive au dernier pilote :
Je commence à devenir FOU. Je n'arrive pas à changer cette foutue image wtf... J'ai beau tenter par tous les moyens de la changer par une image noire, l'image semble toujours rester celle du départ.
Bon j'ai enfin trouvé pourquoi et je n'ai pas envie de dire comment j'ai trouvé... Je pense que l'on a tous droit à son petit jardin secret.
Maintenant ca veut dire que je peux me focus sur le concept important qui est le changement de la création et de la lecture des JSON.
Voici un exemple de preset JSON :
{
"Main": {
"x": 40,
"y": 355,
"width": 3784,
"height": 1438,
"Zones": [
{
"DriverZone": {
"x": 0,
"y": -10,
"width": 3784,
"height": 71,
"Windows": [
{
"Position": {
"x": 47,
"y": 11,
"width": 72
},
"GapToLeader": {
"x": 445,
"y": 13,
"width": 201
},
"LapTime": {
"x": 859,
"y": 14,
"width": 221
},
"DRS": {
"x": 1094,
"y": 13,
"width": 173
},
"Tyres": {
"x": 1270,
"y": 11,
"width": 1452
},
"Name": {
"x": 2727,
"y": 11,
"width": 351
},
"Sector1": {
"x": 3083,
"y": 10,
"width": 253
},
"Sector2": {
"x": 3339,
"y": 14,
"width": 195
},
"Sector3": {
"x": 3518,
"y": 14,
"width": 250
}
}
]
}
}
]
},
"Drivers": [
"Perez",
"Leclerc",
"Sainz",
"Alonso",
"Stroll",
"Russel",
"Verstappen",
"Zhou",
"Ocon",
"Hulkenberg",
"Hamilton",
"Norris",
"Tsunoda",
"Magnussen",
"Piastri",
"Albon",
"Gasly",
"Sargeant",
"Bottas",
"De Vries"
]
}
Je pense que ce qui serait bien ce serait de rajouter un "offsets" qui contienne les 19 écarts restants.
Bon... la structure de ma fabrication de JSON etait trop confuse je trouve alors je l'ai complêtement refaite.
J'ai aussi abandonné l'idée de faire un fichier le plus petit possible car au final on s'en fiche et le plus important c'est que toutes les windows et les zones soient aux bons endroits.
Ca nous fait un fichier d'environs 1300 lignes mais au moins le code pour la serialisation est plutôt clean :
public void SaveToJson(List<string> drivers, string configName)
{
string JSON = "";
JsonObject jsonFileObject = new JsonObject();
//Creating the mainZone object
JsonObject mainZoneObject = new JsonObject();
mainZoneObject.Add("x",MainZone.Bounds.X);
mainZoneObject.Add("y",MainZone.Bounds.Y);
mainZoneObject.Add("width",MainZone.Bounds.Width);
mainZoneObject.Add("height",MainZone.Bounds.Height);
JsonArray driverZonesArray = new JsonArray();
int DriverID = 0;
foreach (Zone driverZone in MainZone.Zones)
{
DriverID++;
JsonObject driverZoneObject = new JsonObject();
driverZoneObject.Add("name","Driver"+DriverID);
driverZoneObject.Add("x", driverZone.Bounds.X);
driverZoneObject.Add("y", driverZone.Bounds.Y);
driverZoneObject.Add("width", driverZone.Bounds.Width);
driverZoneObject.Add("height", driverZone.Bounds.Height);
JsonArray windowsArray = new JsonArray();
JsonObject windowObject = new JsonObject();
foreach (Window window in driverZone.Windows)
{
windowObject.Add(window.Name, new JsonObject {
{ "x", window.Bounds.X },
{ "y", window.Bounds.Y },
{ "width", window.Bounds.Width },
{ "height", window.Bounds.Height }
});
}
windowsArray.Add(windowObject);
driverZoneObject.Add("Windows",windowsArray);
driverZonesArray.Add(driverZoneObject);
}
mainZoneObject.Add("DriverZones",driverZonesArray);
JsonArray driversArray = new JsonArray();
foreach (string driver in drivers)
{
driversArray.Add(driver);
}
mainZoneObject.Add("Drivers",driversArray);
jsonFileObject.Add("Main",mainZoneObject);
JSON = jsonFileObject.ToString();
//Saving the file
string path = CONFIGS_FOLDER_NAME + configName;
if (File.Exists(path + ".json"))
{
//We need to create a new name
int count = 2;
while (File.Exists(path + "_" + count + ".json"))
{
count++;
}
path += "_" + count + ".json";
}
else
{
path += ".json";
}
File.WriteAllText(path, JSON);
}
Et normalement la lecture devrait être encore plus simple.
En fait c'était pas beaucoup plus simple mais au moins maintenant ca marche. Je vais pas mettre le code de lecture ici car c'est un peu trop long donc il va falloir me croire sur parole. (Ou aller sur Git)
Bon bah on est au même endroit qu'hier...
Bon pour demain le plan de bataille ca va être :
Changer complêtement la methode "GetTimeFromPng" pour qu'elle prenne en compte toutes les possibilités de bugs et d'oubli de '.' ou de ':' mais pas selon le nombre de blocs mais selon le type de temps que l'on cherche
Pour le moment je regarde le nombre de blocs et si il y en a deux alors c'est que c'est un temps de secteur. En fait non cela peut aussi être un temps au tour qui a raté un point.
Il faut que je bosse juste un peu vite fait la dessus et que j'arrête de putain de crasher dès que un truc est pas au bon format. Ensuite quand ca aura arrêté de crasher je vais reprendre l'OCR et voir pourquoi les resultats sont nuls a chier comme ca.
Et le but c'est que demain soir j'ai une reconnaissance de caractères plus proche de ce que j'avais dans d'autres projets... J'y croit 0 mais bon l'espoir fait vivre comme on dit.






















































































