Files
TrackTrendsDoc/site/search/search_index.json
T
2023-05-11 15:55:47 +02:00

1 line
528 KiB
JSON

{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"index.html","text":"Rapport Track Trends V1.0 Rohmer Maxime Travail de dipl\u00f4me Technicien ES 2023 Introduction R\u00e9sum\u00e9 Track Trends est un outil de r\u00e9cup\u00e9ration et d'analyse de donn\u00e9es de courses de Formule 1. Pour le contexte, en dehors des cours, j'exerce diff\u00e9rentes activit\u00e9s dont celle de Live Ticker F1 pour le 20 minutes. Pour m'aider dans ce travail, j'utilise actuellement la F1TV \u00e0 laquelle je suis abonn\u00e9 qui me propose non seulement un feed vid\u00e9o de meilleure qualit\u00e9 avec des commentaires plus pertinents que ceux de la RTS mais qui aussi me permet d'acc\u00e9der \u00e0 un feed vid\u00e9o tr\u00e8s important : la chaine data. Ce dernier ressemble \u00e0 cela : \"Screenshot du feed data de la f1tv\" (Attention ce n'est pas un joli tableau HTML, mais bien une vid\u00e9o qui contient un tableau.) Sauf que toutes les informations sont \u00e9tal\u00e9es p\u00eale-m\u00eale sans hi\u00e9rarchie ce qui fait que cela me prendrait trop de temps de tout d\u00e9chiffrer \u00e0 chaque fois, ce qui me fait rater des choses int\u00e9ressantes. Le but du projet est donc de fournir un outil qui hi\u00e9rarchise et affiche diff\u00e9remment les donn\u00e9es pour faciliter leur lecture et me permettre de faire de meilleurs commentaires. Abstract Track Trends is a Formula 1 data is a tool that displays and interpret data. To understand everything,first ,a little bit of context. In my free time I have multiple activities and one is to be the Live Ticker F1 for the local journal \"20 minutes\" (Owned by Tamedia). to help me in this work I'm currently using the F1TV to which I'm currently subscribed because it provides me with a better video feed with better commentary than the ones from the RTS (in my opinion) but also because it gives me access to a very important video feed : the data channel You can see in the chapter above an example of the F1TV DATA CHANNEL. [Note : Even tough it looks like a pretty HTML table on wich you could easely get infos... Its not. Its a video feed] You can see a lot of data all well and good BUT! All the data is displayed the same in a big table which make it really hard to read totally in a hurry, which means that I miss a lot of useful information. The point of the project then is to provide a tool that can display those data by taking into account their relevance. So for example a driver that is 10s away from everyone and that is doing some normal lapTimes will be less displayed or even not displayed at all so I can focus on the drivers that are battling each others. This tool would help me not miss the battles and details that are happening in the back and therefore not being broadcasted on TV.And it could be a usefull tool for anyone who wants a better insight of how the race is going by looking at the data. This kind of project already exists in the form of the AWS tool \"F1 Insight\" but it is not avaible to the public. We can only see some of its predictions (that are trash) and data dumps in the live feed when the TV directors feel like it. Description du besoin Comme expliqu\u00e9 dans le r\u00e9sum\u00e9, je suis Live Ticker F1. Mais pour mieux comprendre le besoin que j'ai, je pense qu'il est pertinent de comprendre comment je travaille. Pendant un Grand Prix de Formule 1 j'ai plusieurs t\u00e2ches \u00e0 effectuer : Suivre les diff\u00e9rents \u00e9v\u00e8nements du Grand Prix Changer le titre et la photo de titre du Live Chercher des Tweets ou des Images \u00e0 int\u00e9grer Ecrire les commentaires en faisant attention \u00e0 dire ce qu'il se passe mais aussi l\u2019expliquer, ce que cela implique, mais aussi ce que cela veut dire pour le reste de la course. Comprendre et expliquer les strat\u00e9gies Tout cela toutes les cinq minutes max... Cela veut dire que je dois \u00eatre le plus rapide possible quand je cherche des informations. Et comme le tableau en comporte trop et bien, je suis oblig\u00e9 de le lire en diagonale. Par exemple dans le tableau, les infos que je cherche le plus sont : Le nombre de places gagn\u00e9es (surtout au d\u00e9part) Les \u00e9carts entre les pilotes (surtout ceux qui sont en dessous de deux secondes). Les pneus de chaque pilote et combien de tours, ils ont fait dessus Les temps d'arr\u00eats aux stands Les temps au tour (surtout pour la strat\u00e9gie) Mais pleins d'autres informations existent si on les recoupe sur plusieurs tours. Un outil qui permettrait de mettre en \u00e9vidence les informations importantes serait donc une tr\u00e8s grosse plus-value pour mon travail et s'il est facile \u00e0 installer et \u00e0 utiliser, il se pourrait qu'il devienne indispensable. Cahier des charges Il s'agit d'une version coup\u00e9e du cahier des charges qui ne contient pas l'explication du contexte. Mais l'original est disponible sur ce site \u00e9galement. Il est toutefois normal d'y voir des choses r\u00e9p\u00e9t\u00e9es ou l\u00e9g\u00e8rement diff\u00e9rentes, en effet, il n'a pas \u00e9t\u00e9 \u00e9crit en m\u00eame temps que le reste de ce document. Projet Un outil de style compagnon sous forme d'application C# Windows Form qui r\u00e9cup\u00e8re en temps r\u00e9el les informations de la course et affiche les informations les plus importantes. Le but est non seulement de faciliter mon job, mais aussi faire en sorte d'am\u00e9liorer la plus-value de mon travail en me permettant de fournir des commentaires qui ne sont pas disponibles pour le tout venant qui regarde simplement le flux RTS. Exemples : Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand Maintenant afficher diff\u00e9remment les infos, c'est sympa, mais cela serait encore mieux de traiter ces data et de permettre des petites pr\u00e9dictions. Exemples : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents R\u00e9alisation Malheureusement, la Formula 1 Management ne propose aucune API publique qui puisse nous permettre de faire ce projet \"simplement\". La raison la plus probable \u00e9tant qu'Amazon avec son service AWS propose exactement ce genre de services pour le flux t\u00e9l\u00e9vis\u00e9 et il doit y avoir un contrat d'exclusivit\u00e9. Il existe des API \"Pirates\" faites par la communaut\u00e9, le probl\u00e8me est qu'elles ne sont pas forc\u00e9ment des plus pratiques \u00e0 utiliser. Mais comme je poss\u00e8de un abonnement premium ++ \u00e0 la F1TV, j'ai acc\u00e8s pour chaque grand prix \u00e0 un flux vid\u00e9o nomm\u00e9 : DATA F1 CHANNEL Qui ressemble \u00e0 \u00e7a : \"Exemple de la Data Channel\" Donc la seule fa\u00e7on que je vois de r\u00e9cup\u00e9rer ces donn\u00e9es est de les prendre directement sur ce feed. M\u00eame si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout \u00eatre la r\u00e9cup\u00e9ration des donn\u00e9es et leur stockage. Les donn\u00e9es viennent du flux vid\u00e9o et ainsi dans un premier temps, il va falloir r\u00e9cup\u00e9rer d'une mani\u00e8re ou d'une autre des images qui viennent d'un grand prix en direct ou en rediffusion. Ensuite, dans un second temps, il faut lire les informations directement sur l'image en utilisant une librairie pr\u00e9vue pour (exemple Tesseract) et v\u00e9rifier l'int\u00e9grit\u00e9 de ces derni\u00e8res pour qu'on puisse ensuite les stocker. Dans un troisi\u00e8me temps, il faut stocker toutes ces donn\u00e9es dans une forme qui permette d'aller facilement faire des requ\u00eates de r\u00e9cup\u00e9ration et d\u00e9j\u00e0 pr\u00e9parer des m\u00e9thodes qui permettent de r\u00e9cup\u00e9rer des infos importantes (ex : la moyenne des cinq derniers tours, le temps moyen d'arr\u00eat etc.) pour faciliter la derni\u00e8re \u00e9tape Quand tout cela est fait, on peut ensuite s'amuser un peu avec les Data. La derni\u00e8re \u00e9tape est donc l'affichage. L'id\u00e9e est de cr\u00e9er une Windows Form qui contienne toutes ces informations dans un format beaucoup plus lisible et avec laquelle on pourrait interagir pour permettre de plus facilement commenter les Grands Prix. (exemple plus bas avec un croquis de ce \u00e0 quoi l'application pourrait ressembler) Voici la liste des donn\u00e9es qui pourraient \u00eatre affich\u00e9es (Non contractuel, simplement des id\u00e9es). Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand La moyenne de temps que les pilotes perdent dans les stands La performance moyenne des 5 types de pneus La moyenne de temps de chaque pilote sur le pneu actuel Le nombre de points que chaque pilote gagnerait selon sa position Le classement de la course Voire m\u00eame si c'est possible : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents Pr\u00e9dire les temps au tour de chaque pilote selon l'usure des pneus Voici un exemple d'interface possible pour une page : \"Protype de l'app fait sur Figma\" Cas d'utilisation '*'On va consid\u00e9rer que tous les user ont un abonnement F1 TV PRO Un user veut r\u00e9cup\u00e9rer les data : Il ouvre son navigateur et lance la page DATA de la F1 TV Il calibre la capture des data via le programme (pour la premi\u00e8re utilisation). Il confirme que les donn\u00e9es initiales sont bonnes (pour la premi\u00e8re utilisation). Il regarde tranquille son Grand Prix Le programme r\u00e9cup\u00e8re les data : Il r\u00e9cup\u00e8re des images depuis la F1TV Il utilise Tesseract (ou autre) pour en r\u00e9cup\u00e9rer les infos. Il met ces infos dans un Objet Pilote, dans un Objet course avec un attribut tour pour hi\u00e9rarchiser les data Pour ce qui est de l'affichage, l'id\u00e9e est de faire une application C# comme on l'a appris \u00e0 l'\u00e9cole, mais avec assez de style pour qu'elle puisse \u00eatre agr\u00e9able \u00e0 utiliser. Quand le programme affiche les data : Il prend les donn\u00e9es venant directement de la F1TV. Il affiche diff\u00e9remment les donn\u00e9es pour permettre une meilleure lisibilit\u00e9 Il interpr\u00e8te avec des r\u00e8gles plut\u00f4t simples certaines data pour faire des minipr\u00e9dictions ou aider \u00e0 la lecture Il r\u00e9cup\u00e8re des infos d'autres courses pour les comparer et proposer des pr\u00e9dictions plus int\u00e9ressantes Difficult\u00e9s techniques R\u00e9cup\u00e9rer un flux vid\u00e9o plut\u00f4t propre malgr\u00e9 les contres mesures de la F1 TV pour en emp\u00eacher la lecture par un logiciel Si on doit passer par une capture d'\u00e9cran, trouver un moyen de stocker les donn\u00e9es de mani\u00e8re \u00e0 pr\u00e9voir que parfois un tour pourrait avoir plus de donn\u00e9es qu'un autre, que le user peut mettre pause, ou m\u00eame qu\u2019il revienne en arri\u00e8re. D\u00e9velopper des algorithmes pour r\u00e9cup\u00e9rer les donn\u00e9es comme les diff\u00e9rents pneus utilis\u00e9s ou l'activation du DRS ainsi que d\u00e9velopper des moyens de nettoyer les r\u00e9sultats de l'OCR (Par exemple utiliser diff\u00e9rents champs redondants pour comparer les r\u00e9sultats) Stocker les donn\u00e9es sur une base pour les traiter plus tard tout en pr\u00e9voyant un moyen de voir les stats live D\u00e9velopper des algorithmes de pr\u00e9diction qui prennent en compte d'anciennes courses pour tenter de pr\u00e9dire des choses comme les arr\u00eats aux stands par exemple. Diff\u00e9rences sur le cahier des charges [\u00c0 remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me] Planning pr\u00e9visionnel Mes suiveurs m'ont demand\u00e9 un planning de type GANTT pour ce travail de dipl\u00f4me Je n'ai pas utilis\u00e9 un logiciel particulier pour faire ce dernier, mais je me suis inspir\u00e9 des principes fondamentaux d'un diagramme de ce type. Comme l'original a \u00e9t\u00e9 fait sur Excel, je ne peux pas vraiment l'ins\u00e9rer de mani\u00e8re lisible ici, mais il est disponible dans le dossier Planning. Mais voici un r\u00e9sum\u00e9 de son contenu : T\u00e2ches J'ai d\u00e9cid\u00e9 de d\u00e9composer mon planning en trois grands types de t\u00e2ches. Programmation Documentation Tests L'id\u00e9e est de permettre une meilleure lisibilit\u00e9 et de me permettre \u00e0 moi de me faire plus facilement \u00e0 l'id\u00e9e de ce qu'il m'attend. Voici la liste des t\u00e2ches par rubrique : PT Cette rubrique contient les t\u00e2ches qui n'ont pas leur place dans les trois cat\u00e9gories principales. PT1 / pr\u00e9paration au travail de dipl\u00f4me (2) Cette t\u00e2che est un peu hors cat\u00e9gorie, mais c'est normal, c'est une supert\u00e2che qui regroupe beaucoup de choses. C'est une t\u00e2che qui est planifi\u00e9e pour deux jours et qui normalement devrait \u00eatre faite les deux premiers jours du travail. Le but est de pr\u00e9parer tout ce qui peut \u00eatre pr\u00e9par\u00e9 en avance niveau documentation et mise en place pour ne pas avoir besoin de s'en soucier ensuite. DT Rubrique documentation qui contient toutes les t\u00e2ches en rapport de pr\u00e8s ou de loin avec la documentation du projet. DT1 Cr\u00e9ation du poster (1) Cette t\u00e2che consiste \u00e0 faire une version num\u00e9rique du poster qui soit en accord avec les consignes qu'on nous a donn\u00e9es. Le but est aussi et surtout de faire poster dont je sois fier et que je sois content de montrer. Il y a d\u00e9j\u00e0 des croquis de poster et j'ai clairement pr\u00e9vu de travailler sur \u00e7a pendant les vacances alors, je n'ai mis qu'un jour et je l'ai plac\u00e9 juste avant le rendu de ce dernier. DT2 Documentation Analyse de l'existant (2) Cette t\u00e2che est d\u00e9di\u00e9e \u00e0 l'\u00e9criture de la documentation et plus pr\u00e9cis\u00e9ment de l'analyse de l'existant. Comme il y a pas mal de technologies utilis\u00e9es dans mon projet, j'aimerais faire correctement un vrai debrief de pourquoi j'ai utilis\u00e9 l'une ou l'autre alors, j'ai assign\u00e9 deux jours dessus. DT3 Documentation Analyse organique (5) Cette t\u00e2che est la plus grosse dans la cat\u00e9gorie documentation. Il s'agit de documenter comment l'application fonctionne. J'y ai mis cinq jours et je pense que c'est un minimum car c'est dans cette t\u00e2che que je vais devoir d\u00e9tailler exactement comment fonctionne chaque partie du projet. Ces cinq jours sont \u00e9parpill\u00e9s sur le projet en g\u00e9n\u00e9ral \u00e0 la fin du d\u00e9veloppement de chaque grande partie de projet. Le but est de ne rien oublier et de ne pas avoir \u00e0 tout faire en m\u00eame temps. DT4 Documentation Analyse fonctionnelle (2) Cette t\u00e2che est d\u00e9j\u00e0 moins grosse, elle consiste \u00e0 documenter le fonctionnement de l'application et comment utiliser les composants que j'ai d\u00e9velopp\u00e9s. Je l'ai mis en fin de projet, car comme j'ai l'habitude de faire des analyses fonctionnelles plut\u00f4t pr\u00e9cises, le moindre changement dans l'UI peut tout rendre obsol\u00e8te. J'y ai mis deux jours, car j'aimerais correctement documenter avec de bonnes photos et sc\u00e9narios pour qu'on puisse voir toutes les possibilit\u00e9s de l'application. DT5 Documentation Tests (1) Cette t\u00e2che est un peu plus petite qu'elle ne le devrait. Elle concerne la documentation des diff\u00e9rents tests. Je n'y ai mis qu'un seul jour, car en r\u00e9alit\u00e9 les diff\u00e9rentes t\u00e2ches de tests contiennent aussi beaucoup de documentation, DT6 Documentation Reste (2) Cette t\u00e2che est une t\u00e2che un peu vague. Elle contient toutes les actions autres que j'aurai besoin de faire (Mise au propre, orthographe, g\u00e9n\u00e9ration de PDF ...). J'y ai mis deux jours pour avoir un peu de marge, car ce sont toujours des t\u00e2ches qui paraissent faciles, mais qui \u00e0 la fin prennent beaucoup de temps si on les fait bien. PT Rubrique programmation qui contient toutes les t\u00e2ches qui touchent \u00e0 la programmation et au d\u00e9veloppement de l'application. PT1 Programmation r\u00e9cup\u00e9ration des images (3) Cette t\u00e2che est estim\u00e9e \u00e0 seulement trois jours, il ne faut pas s'y m\u00e9prendre, c'est une des t\u00e2ches les plus dures et lourdes niveaux documentation en explications. Cependant, un POC (Proof Of Concept) assez avanc\u00e9 a d\u00e9j\u00e0 \u00e9t\u00e9 fait et donc cela permet de n'envisager que trois jours, car il suffit de l'impl\u00e9menter et de la paufinner. Cette t\u00e2che consiste \u00e0 prendre en entr\u00e9e un lien de Grand Prix et de sortir une image tous les x secondes de la page DATA. Cela peut sembler simple, mais pour le faire sans prendre d'espace d'\u00e9cran et ne demandant pas \u00e0 l'utilisateur de copier-coller quoi que ce soit o\u00f9 de donner ses identifiants F1TV c'est un challenge. Cela peut paraitre curieux alors de mettre cette t\u00e2che loin dans le planning m\u00eame si c'est la premi\u00e8re \u00e9tape du projet. Encore une fois cela s'explique avec le fait qu'il y a d\u00e9j\u00e0 un POC qui fonctionne \u00e0 peu pr\u00e8s et que donc pr\u00e9f\u00e8re commencer avec des t\u00e2ches plus incertaines dans le cas o\u00f9 elles prendraient plus de temps que pr\u00e9vu. PT2 Programmation OCR (5) Cette t\u00e2che consiste \u00e0 d\u00e9velopper la partie qui reconnait le texte sur les images. C'est tr\u00e8s certainement la t\u00e2che qui risque le plus de d\u00e9border car c'est celle qui est la plus complexe techniquement puisqu'elle demande non seulement la lecture sur image, mais aussi le d\u00e9veloppement d'algorithmes de traitement de cette donn\u00e9e pour \u00eatre s\u00fbr qu'elle a bien \u00e9t\u00e9 lue. J'y ai ainsi allou\u00e9 cinq jours, mais j'esp\u00e8re que j'arriverai \u00e0 gagner du temps sur les autres pour y allouer plus dans le planning effectif, car je suis convaincu que plus, on y passe du temps, meilleur sera le r\u00e9sultat. PT3 Programmation, stockage et mod\u00e8le (5) Cette partie est moins technique, mais concerne le stockage des donn\u00e9es que nous retourne la lecture des images. Mais elle va demander de la r\u00e9flexion et de l'intelligence de programmation, car il faut que cette partie anticipe les besoins de la vue et pr\u00e9pare un terrain fertile qui ne demande pas un refactor quand on passera au d\u00e9veloppement de la vue. C'est pour cela que je lui ai aussi assign\u00e9 cinq jours de travail et elle doit absolument \u00eatre commenc\u00e9e apr\u00e8s la lecture. PT4 Programmation Vue de l'APP (5) Cette partie peut \u00eatre horrible comme tr\u00e8s facile, cela d\u00e9pend compl\u00e8tement de la qualit\u00e9 du travail avant. Si le mod\u00e8le est parfait et que les donn\u00e9es sont int\u00e8gres, cela devrait \u00eatre plut\u00f4t simple de les afficher de mani\u00e8re int\u00e9ressante. Cependant, cette partie d\u00e9bordera s\u00fbrement un peu, car tout le temps gagn\u00e9 avec de bonnes donn\u00e9es sera utilis\u00e9 pour tenter de faire de la pr\u00e9diction. Pour ces raisons, je lui ai assign\u00e9 \u00e9galement cinq jours de travail et elle doit absolument \u00eatre faite apr\u00e8s le mod\u00e8le. PT5 Programmation mise en commun (3) Cette t\u00e2che est aussi un petit peu sp\u00e9ciale, car elle regroupe plusieurs choses. En gros, chaque partie de programmation sera s\u00fbrement assez ind\u00e9pendante et il faudra \u00e0 un moment faire un seul projet C# qui contient tout. Il est difficile d'estimer \u00e0 quel point cela va \u00eatre compliqu\u00e9 alors, j'ai \u00e9t\u00e9 conservateur et j'ai mis trois jours. TT Cette rubrique contient les t\u00e2ches qui sont uniquement des tests. La plupart des t\u00e2ches de programmations contiennent d\u00e9j\u00e0 des tests, mais certaines demandent une attention particuli\u00e8re. TT1 Tests OCR (2) Cette t\u00e2che est une des t\u00e2ches les plus importantes. Son but est de faire un protocole de tests complet qui permette de comparer les diff\u00e9rents algorithmes de reconnaissance de texte. Je l'ai mise apr\u00e8s la reconnaissance, mais m\u00eame maintenant en \u00e9crivant ces lignes, je me dis que dans le planning effectif, elle sera faite pendant la t\u00e2che de programmation. En effet, comment savoir si mon tout nouvel algorithme est r\u00e9ellement mieux que le pr\u00e9c\u00e9dent. Je pr\u00e9vois deux jours, car je pense que faire le dataset va prendre beaucoup de temps, il faut pr\u00e9voir un certain nombre d'images et de texte qui pourront ensuite \u00eatre donn\u00e9es sous forme de tests. C'est certes une t\u00e2che de test, mais c'est aussi de la programmation. TT2 Tests finaux (2) Cette t\u00e2che de tests est un peu vague, elle regroupe les diff\u00e9rents tests pour v\u00e9rifier que les donn\u00e9es sont bien affich\u00e9es correctement. Ce qui serait cool si j'ai du temps en fin de travail de dipl\u00f4me serait de faire un syst\u00e8me de test qui permet d'entrainer le programme \u00e0 mieux reconnaitre certaines choses comme des arr\u00eats aux stands si on lui donne les trois derni\u00e8res ann\u00e9es de grands Prix. J'ai mis une dur\u00e9e arbitraire de deux jours, mais je ne sais pas vraiment combien de temps cela va vraiment durer. Elle est \u00e9videmment \u00e0 effectuer une fois que tout est \u00e0 peu pr\u00e8s termin\u00e9. Planning effectif et diff\u00e9rences [A remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me] Analyse fonctionnelle [A remplir au fur et \u00e0 mesure dans la seconde moiti\u00e9 du travail de dipl\u00f4me] Analyse Organique [A remplir au fur et \u00e0 mesure que les features sont d\u00e9velopp\u00e9es] R\u00e9cup\u00e9ration des images Voici la premi\u00e8re grande \u00e9tape du projet. Pour rappel, Amazon h\u00e9berge directement le site de la F1TV et poss\u00e8de les droits sur les donn\u00e9es de la F1. C'est sous le nom de AWS (le service d'h\u00e9bergement d'Amazon) que la firme apparait en tant que sponsor. On peut voir ce nom appara\u00eetre assez souvent quand on regarde un Grand Prix car comme ils ont la main-mise sur les donn\u00e9es ils peuvent ins\u00e8rer des bandeaux d'informations sur le flux public sur ce qu'il se passe voir m\u00eame faire des pr\u00e9dictions (Bien qu'un peu bancales) \"Exemple insertion AWS en GP\" Ce service s'appelle F1 Insights (Oui c'est un meilleur nom de projet que F1 Companion mais bon) et c'est, je pense, la raison pour laquelle on ne voit aucune API publique qui permette de correctement se renseigner en don\u00e9es en direct pendant un Grand Prix. Ils ont du d\u00e9gotter un juteux contrat pour s'occuper de toute l'infrastructure digitale de la F1 (du moins publique) en \u00e9change d'une exclusivit\u00e9 totale sur certaines choses comme les Data \"Exemple data d'AWS\" Evidemment je ne fais que conjecturer et ce que j'ai dit n'est pas \u00e0 prendre au pied de la lettre mais c'est une explication possible je pense de pourquoi il est si difficile de trouver des donn\u00e9es sur la F1 facilement en temps r\u00e9el. Il existe bien quelques API un peu bancales publiques, mais le probl\u00e8me c'est qu'elles ne sont vraiment pas suffisante et je ne peux pas leur faire confiance quand je commente. Ce qu'il m'aurait fallut c'est une API publique et officielle qui me permette d'\u00eatre sur que les donn\u00e9es sont les bonnes et qu'elles arrivent le plus vite possible. On pourrait croire que c'est impossible car cela n'existe pas comme je l'ai dit MAIS ! Ce n'est pas compl\u00eatement vrai. En effet depuis que je poss\u00e8de un abonnement \u00e0 la F1TV, il existe une source d'informations tr\u00e8s pr\u00e9cieuse qui m'aide \u00e9norm\u00e9ment dans mon quotidien de commentateur de Formule 1. La \"DATA CHANNEL\". La Data Channel est une page de la F1TV qui permet, pour chaque Grand Prix, de visualiser, sous la forme d'un flux vid\u00e9o, diff\u00e9rentes informations capitales sur la course. \"Exemple de Data Channel\" Le probl\u00e8me, c'est que comme je viens de le dire, ces donn\u00e9es ne sont pas accessibles comme un tableau HTML ou un flux RSS ou un tableau JSON. C'est un flux vid\u00e9o. Il faut savoir qu'entretenir une diffusion de flux vid\u00e9o en 1080P pendant deux heures accessible par des milliers d'abonn\u00e9s est EXTR\u00caMENT cher surtout quand on le compare \u00e0 simplement afficher les donn\u00e9es dans un tableau. Ce qui veut dire que ce choix est d\u00e9lib\u00e9r\u00e9 et a un sens au niveau \u00e9conomique. Je pense donc que c'est justement pour \u00e9viter que des petits malins puissent simplement venir scraper l'int\u00e9gralit\u00e9 des donn\u00e9es qu'ils proposent et fasse sa propre API. (C'est d'ailleurs un des sites avec la meilleure protection anti bot du monde) MAIS ce n'est pas par ce que les donn\u00e9es ne sont pas facile \u00e0 avoir qu'elles sont IMPOSSIBLE \u00e0 avoir. Et c'est la que ce projet entre en jeu. Mais pour d\u00e9coder les donn\u00e9es d'une image il faut dabord ... (roulement de tambours) ... Avoir des images ! Et c'est la que vient se glisser cette partie du projet. Comment faire ? Le but de ce segment est de se concentrer sur la r\u00e9cup\u00e8ration et la mise \u00e0 disposition pour le reste du programme, des images en direct de la F1TV dans la meilleure qualit\u00e9 possible et dans les meilleurs d\u00e9lais. Pour ce faire il y a plusieurs solutions : Reverse engeneer la F1TV pour acc\u00e8der directement au flux sans passer par la plateforme internet et pouvoir prendres images \u00e0 volont\u00e9. Avoir tout simplement une page de la F1TV ouverte sur un \u00e9cran et prendres des screenshots \u00e0 intervals r\u00e9guliers. Simuler un navigateur internet sans qu'il soit affich\u00e9 et le contr\u00f4ler automatiquement pour qu'il prenne des captures. La premi\u00e8re option aurait \u00e9t\u00e9 la plus \u00e9l\u00e9gante mais lors d'un POC que je tentais de r\u00e9aliser je me suis rendu compte que cela serait un peu trop compliqu\u00e9 et long \u00e0 faire. Sans compter le fait que les rediffusions de Grand Prix ne sont pas g\u00e8r\u00e9es de la m\u00eame mani\u00e8re que les diffusions en live. Et que pour faire des Tests en live il faudrait attendre \u00e0 chaque fois un weekend de Grand Prix et le faire en plus du commentaire que je dois produire. Pour toutes ces raisons et bien d'autres je l'ai rang\u00e9e dans la case \"Trop dur, Trop chiant, S\u00fbrement ill\u00e9gal\" (Oui je sais c'est une cat\u00e9gorie bien sp\u00e9cifique mais c'est ma documentation je fais ce que je veux) La troisi\u00e8me option aurait \u00e9t\u00e9 la plus simple (et moins dr\u00f4le) et je suis presque s\u00fbr que je peux impl\u00e9menter cette derni\u00e8re en moins d'une apr\u00e8s-midi. Sauf qu'elle apporte de gros soucis. On ne peux pas garantir l'int\u00e9grit\u00e9 et la continuit\u00e9 des donn\u00e9es si l'utilisateur avance ou fait pause m\u00eame par simple inadvertance. La moindre fen\u00eatre qui s'afficherait devant ruinerait toute la reconnaissance de caract\u00e8res. On ne peut pas contr\u00f4ler la qualit\u00e9 du flux et on est oblig\u00e9 de faire confiance en l'utilisateur On ne peut pas vraiment automatiser quoi que ce soit niveau tests ou m\u00eame pour faire du scrapping auto pour remplir une base de donn\u00e9e. Et finalement le pire inconv\u00e9nient : C'EST NUL ! Je ne pourrais jamais utiliser un projet qui fonctionne de cette facon, je ne peux pas me permettre d'avoir un \u00e9cran inutilisable quand je commente et auquel je dois constamment faire attention pour ne pas perturber la reconnaissance. Pour moi cette option aurait \u00e9t\u00e9 celle \u00e0 choisir en cas d'extr\u00eame urgence et en dernier recours car le projet deviendrait inutile. J'ai donc d\u00e9cid\u00e9 de m'occuper de la seconde option : Simuler un navigateur. Cette option bien que complexe et difficile \u00e0 impl\u00e9menter propose une solution \u00e0 tous les probl\u00eame et permet une r\u00e9cup\u00e8ration quasi sans compromis. Simuler un navigateur ? Simuler un navigateur internet n'est pas forc\u00e9ment tr\u00e8s difficile. Chromium par exemple offre une panoplie d'outils natifs et \u00e9norm\u00e9ment de librairies existent permettant de facilement et en quelques lignes simuler un Google Chrome et le contr\u00f4ler sans afficher son UI. \"Chromium logo\" {: style=\"height:150px;width:150px\"} Cependant. La F1TV n'utilise pas simplement un player HTML5 basique. Elle utilise un service de streaming BitMovin qui permet de fournir un stream de bonne qualit\u00e9 et surtout qui impl\u00e9mente les DRM (Digital Right Management) Cela veut dire que quand on ouvre un flux de la F1TV sur chrome et que l'on essaie de prendre une capture d'\u00e9cran, le player se met en noir et ne permet pas de voir quoi que ce soit (Certaines version de Chrome le permettent pendant quelques semaines avant de bloquer \u00e0 nouveau). Ce qui dans notre cas est un immense probl\u00e8me. Mais Firefox ne nous bloque pas de cette facon et il est donc assez facile de passer outre. L'explication sans trop rentrer dans les d\u00e9tails est la suivante : Dans chrome, le player par d\u00e9faut utilise une technologie appell\u00e9e \"PCP\" ou \"Protected Content Playback\" qui leur permet de bloquer au moins une partie des techniques de r\u00e9cup\u00e8ration du flux vid\u00e9o et audio. Cependant Firefox de pas sa nature Open Source utilise \"OpenH264\" pour lire ces m\u00eames flux soumis \u00e0 des DRM et OpenH264 n'impl\u00e9mente pas les m\u00eames restrictions. Sauf que Firefox n'est pas aussi facilement \u00e9mul\u00e9 que chrome et cela r\u00e9duit notre choix de librairies \u00e0 ... Une seule... Qui est Selenium. (Il existe aussi Pupetteer C# mais j'ai rencontr\u00e9 \u00e9norm\u00e9ment de soucis avec cette derni\u00e8re d\u00e8s que je voulais lancer une vid\u00e9o) \"Firefox dev logo\" {: style=\"height:150px;width:150px\"} Mais m\u00eame si la documentation est plut\u00f4t maigre parfois, c'est une bonne librairie qui permet de tr\u00e8s bien contr\u00f4ler une instance de chrome ou de Firefox. Contr\u00f4ler le navigateur Maintenant que l'on sait quel navigateur simuler et avec quelle technologie, on peut passer \u00e0 la r\u00e9alisation. Ce qu'il y a de bien avec Selenium, c'est qu'on a un certain nombre de commandes tr\u00e8s haut niveau qui nous permettent de contr\u00f4ler un navigateur de mani\u00e8re plut\u00f4t pr\u00e9cise. Je vais d\u00e9crire ici la proc\u00e9dure habituelle utilis\u00e9e sous une forme de recette de cuisine pour que l'on puisse facilement comprendre ce qu'il se passe. Durant cette explication je vais parler \u00e0 un moment de Cookies, ne vous en faites pas c'est le sous chapitre suivant qui va vous en parler. Recette de cuisine pour r\u00e9cup\u00e8rer des images de la F1TV : D\u00e9marrer une instance de navigateur avec les bons arguments Ajouter les bons param\u00eatres pour ne pas se faire flag comme un bot Naviguer sur la page de la F1TV Ajouter les cookies de connexion pour avoir acc\u00e8s au contenu de la page Naviguer sur la page du Grand Prix demand\u00e9 Attendre un peu que la page se charge Cliquer sur l'invite de cookies Attendre cinq secondes le temps que la page se reload Cliquer sur le bouton qui permet de passer du feed live \u00e0 la DATA CHANNEL Appuyer sur Espace pour faire apparaitre le bouton d'acc\u00e8s au param\u00eatres Cliquer sur le menu d\u00e9roulant des r\u00e9solution Trouver l'option 1080P et la selectionner Cliquer sur le bouton qui met la vid\u00e9o en plein \u00e9cran Prendre de screenshots \u00e0 intervales r\u00e9guliers Pour faire toutes ces actions on doit r\u00e9cup\u00e8rer les \u00e9l\u00e9ments selon leur ID ou leur classe. Voici un exemple qui r\u00e9cup\u00e8re le bouton de plein \u00e9cran et qui clique dessus : IWebElement fullScreenButton = Driver.FindElement(By.ClassName(\"bmpui-ui-fullscreentogglebutton\")); fullScreenButton.Click(); Ca peut para\u00eetre plut\u00f4t simple dit comme ca et quand tout fonctionne ca l'est mais la difficult\u00e9 vient du fait que \u00e0 peu pr\u00e8s nimporte laquelle de ces \u00e9tapes peut rater et qu'il faut donc faire un bon syst\u00e8me de gestion d'erreurs qui puisse aider l'utilisateur en cas de probl\u00e8me. Il faut dire aussi que les sites ne sont pas forc\u00e9ment tr\u00e8s content de voir des bots passer car cela peut \u00eatre un risque de DDOS et de Scraping (Comme moi) et donc ils mettent en place des syst\u00e8mes pour nous emp\u00eacher de faire ce que l'on veut On peut utiliser diff\u00e9rntes techniques pour passer outre ces restrictions comme : Changer son UserAgent Changer sa r\u00e9solution Ne pas avoir des patterns trop pr\u00e9visibles Avoir un historique Ne pas cliquer pile sur le milieu des boutons Ne pas cliquer trop vite Passer par un proxy pour ne pas se faire flag Utiliser des librairies plus discr\u00e8tes J'ai eu l'occasion de tester toutes ces methodes pour tenter de passer derri\u00e8re les radars de la F1TV et visiblement j'ai r\u00e9ussi pour les pages principales mais pas pour les pages de Login. Il faut savoir que la bataille entre bots et propri\u00e9taires de sites est un grand jeu du chat et de la souris et que les plateformes innovent constamment leur s\u00e9curit\u00e9. Et il se trouve que la partie login de la F1TV est heberg\u00e9e autre part que le reste du site chez Amazon et que elle poss\u00e8de les meilleures s\u00e9curit\u00e9s que j'aie pu voir. Aucunes des methodes que j'ai cit\u00e9es et d'autres encore que j'ai essay\u00e9 n'ont r\u00e9ussi \u00e0 fourvoyer le syst\u00e8me. J'ai donc \u00e9t\u00e9 oblig\u00e9 de faire appel \u00e0 la connexion par Cookies pour pouvoir acc\u00e8der au reste du site internet. R\u00e9cup\u00e8rer les cookies ? Alors, on va mettre de c\u00f4t\u00e9 toutes les questions de s\u00e9curit\u00e9 et de violation de la vie priv\u00e9e et de protection des donn\u00e9es des utilisateurs pour ce chapitre. Car pour faire simple, je siphonne TOUS les cookies de la persone qui utilise mon app. [FINIR CETTE EXPLICATIOn] Calibration [AJOUTER EXPLICATION] OCR Ici je vais parler de la seconde partie du projet qui parle du processus de reconnaissance de data sur une image du feed DATA de la F1TV. C'est je pense la partie qui a demand\u00e9 le plus tests et de refactor. Toute la partie OCR a \u00e9t\u00e9 d\u00e9velopp\u00e9e dans un projet \u00e0 part avant d'\u00eatre int\u00e9gr\u00e9e dans le projet final. Il faut savoir que la reconnaissance est diff\u00e9rente celon ce que l'on cherche. Je vais donc d\u00e9composer cette partie du document en sous rubriques selon les donn\u00e9es recherch\u00e9es. Mais avant ca je dois expliquer certains concepts qui seront importants. Fonctionnement g\u00e9n\u00e9ral Voici un screenshot de la page DATA de la F1TV que le programme va recevoir : \"Screen F1TV\" Si on regarde de loin on peut se dire que la structure est plut\u00f4t simple mais c'est loin d'\u00eatre le cas. On peut y voir au moins 4 zones contenant de l'information dans un format diff\u00e9rent. \"Zones principales\" Dans l'exemple ci dessus on peut voir 3 zones mais on aurait \u00e9galement pu comprendre la zone de position des pilotes autour du circuit pour faire 4. Ces 4 zones sont tr\u00e8s diff\u00e9rentes et contiennent d'autres informations. Pour ce travail de dipl\u00f4me je ne m'occupe que de la zone principale. Mais je pense que le titre et les infos de circuit ne prendrait pas tant de temps que ca \u00e0 impl\u00e9menter. J'ai utilis\u00e9 le mot \"Zone\" plus haut et ca n'est pas juste un mot utilis\u00e9 au hasard. C'est le nom de l'objet que j'utilise pour les repr\u00e9senter dans mon programme. Mais comme c'est important de bien comprendre ce concept avant de continuer je vais vous l'expliquer. ZONE : L'objet \"Zone\" parent est un objet qui est une zone d'image. Je m'explique, le but d'une zone est d'\u00eatre un morceau d'une image plus grande. Le but d'une Zone est de contenir une liste de plus petites Zones ou bien une liste de \"Window\" (j'explique ce que c'est juste apr\u00e8s). Elle contient la portion d'image qui la concerne et ses propres dimensions. Le parent zone ne pr\u00e9voit que de pouvoir ajouter ou supprimer des \u00e9l\u00e9ments des listes de zones ou de windows ainsi qu'une methode qui permet d'aller chercher toutes informations des livres qu'elle contient. L'int\u00e9r\u00eat d'une zone est de pouvoir compartimenter une image dans des parties int\u00e9ressantes au niveau de la reconnaissance mais pas de traiter d'information. WINDOW : L'objet \"Window\" est un objet qui peut ressembler beaucoup \u00e0 l'objet \"Zone\". En effet elle aussi est une partie d'une image plus grande et contient ses dimensions, mais elle se distingue en deux points importants. Elle ne contient pas d'autres Zones ou Windows Elle peut retourner les informations \u00e9crites sur son image. Toutes les Window qui h\u00e9ritent du parent Window peuvent impl\u00e9menter une methode qui permet de renvoyer ce qui peut \u00eatre d\u00e9cod\u00e9 sur son image. Les enfants peuvent aussi aller piocher dans les nombresues methodes de r\u00e9cup\u00e8ration de donn\u00e9es contenues dans le parent Window. Mieux vaut r\u00e9utiliser le plus possible que de r\u00e9inventer la roue pour chaque Window. Une analogie un peu bancale pourrait se pr\u00e9senter comme la suivante : La zone est une armoire ou une bibliot\u00e8que. Si c'est une zone qui contient d'autres zones c'est une bibliot\u00e8que et chacune de ces sous-zones sont des armoires. Leur unique but est de contenir de mani\u00e8re ordonn\u00e9e des objets qui eux contiennent de l'information. Les livres ici sont les Windows. Ils contiennet de l'information et sont stock\u00e9s dans des armoires et on y acc\u00e8de en allant dans la bonne bibliot\u00e8que et en allant dans la bonne armoire. Derni\u00e8res choses pour comprendre le diagramme: Il existe une Main Zone qui est une des 4 grandes zones dont je parlais dans la d\u00e9composition de l'image. Il existe aussi des \"Driver Zone\" qui sont de plus petites zones contenues dans la Main Zone qui et qui ne contiennent que les informations d'un pilote. L'objet Window n'est quasi jamais utilis\u00e9, c'est presque tout le temps des enfants de Window plus sp\u00e9cifiques qui sont utilis\u00e9s, le but est que chaque type d'information sur l'image aie son type de window. Voila donc un petit diagramme qui montre le d\u00e9coupage du programme : \"Diagramme explicatif de l'architecture des zones\" Pour visualiser encore un peu mieux comment ce d\u00e9coupage prend forme voici ce que chaque zone et Window contient. Main Zone : \"Exemple zone principale\" Driver Zone : \"Exemple zone de pilote\" Driver Position Window : \"Exemple de fen\u00eatre de position\" Driver name Window : \"Exemple de fen\u00eatre de nom\" Driver LapTime Window : \"Exemple de fen\u00eatre de temps au tour\" Driver Tyre Window : \"Exemple de fen\u00eatre pneus\" Il existe d'autres types de Window mais ce sont les principaux. On se rend assez facilement compte que chacunes de ces windows va avoir besoin d'un traitement sp\u00e9cifique car la mani\u00e8re de reconnaitre le pneu utilis\u00e9 et le temps au tour ne peut pas \u00eatre la m\u00eame. Pour r\u00e9sumer, on a un programme qui prend en entr\u00e9e un fichier de configuration, qui prend des images de la F1TV et les d\u00e9coupe dans des ZONES qui elles m\u00eame sont d\u00e9coup\u00e9es en WINDOWS pour qu'on puisse plus facilement les d\u00e9coder. Maintenant qu'on a une liste de diff\u00e9rent types de zones on peut commencer \u00e0 chercher ce qu'il y a marqu\u00e9 dessus. Pour cela il faut dabord comprendre un petit peu comment l'OCR fonctionne et comment des libraries comme Tesseract fonctionnent pour donner du texte en partant d'une image. Pour faire tr\u00e8s simple, nous avons un mod\u00e8le qui est entrain\u00e9. C'est \u00e0 dire que on donne \u00e0 un programme un tr\u00e8s grand nombre de mots ou de lettres en lui disant ce que contiennent chaques images. Ensuite le programme va cr\u00e9er des matrices de convolutions pour chaque lettre avec comme objectif de detecter les points communs entre les lettres pour cr\u00e9er un alpphabet. Par exemple la matric de la lettre 'H' donnerait un poids important \u00e0 des lignes verticales connect\u00e9es par une ligne centrale. Et si on fournis assez de donn\u00e9es de bonne qualit\u00e9 au mod\u00e8le, les matrices peuvent \u00eatre tr\u00e8s efficace \u00e0 detecter si une lettre est un H ou un M. Il y a pleins d'autres methodes comme l'utilisation d'un dictionnaire de mots de la langue pour permettre la reconnaissance de mots m\u00eame si une lettre au milieu n'est pas comprise ou en ajoutant d'autres informations sur le contexte mais ca ne nous int\u00e9resse pas ici. C'est important de comprendre comment cette reconnaissance de caract\u00e8res avec des matrices fonctionne car cela va nous aider \u00e0 pr\u00e9parer nos donn\u00e9es pour lui rendre la vie facile et augmenter la pr\u00e9cision de nos r\u00e9sultats. Filtres et traitement On peut essayer de donner toutes nos images directement \u00e0 Tesseract pour qu'il reconnaisse tout le texte qu'il y voit mais on risque de se retrouver avec des r\u00e9sultats au mieux inconsistents. Dans notre cas, le soucis est que les chiffres et lettres sont beaucoup trop petits. Ils ne font parfoisd que 10 pixels de haut et cela fait que il n'est pas forc\u00e9ment facile de toujours les diff\u00e9rencier. De plus, comme ils sont petits, les art\u00e9facts d'aliasing sont assez violents et peuvent grandement d\u00e9former une lettre ou un chiffre. Exemple : Prenons le chiffre 9. Dans l'image il peut \u00eatre repr\u00e9sent\u00e9 de cette mani\u00e8re : \"Exemple de chiffre avant post traitement\" On peut voir qu'il est flou, pour nous cela ne pose pas de probl\u00e8me et je pense que \u00e0 peu pr\u00e8s nimporte qui peut dire que c'est un 9. Cependant comme les contours sont flous et m\u00eame si on essaie de retirer le background : \"9 avec anti aliasing\" On voit que le 9 n'est pas clairement d\u00e9finit. En effet on pourrait le comprendre comme : \"Premier exemple de contours\" Ou comme : \"Second exemple de contours\" Voire m\u00eame simplement comme : \"Exemple de coutour g\u00e9n\u00e9reux\" Et on se rend bien compte que les performances de detection ne sont pas les m\u00eames dans ces trois cas. Il faut donc faire un certain post traitement des images pour supprimer les \u00e9l\u00e9ments parasites, les couleurs, et augmenter la visibilit\u00e9 des contours importants. Mais chaque type de donn\u00e9e va avoir des methodes de post traitement diff\u00e9rents. Donc voici les diff\u00e9rents types de reconnaissance et leur post traitements : Texte Alors ce type de reconnaissance est utilis\u00e9 par la WINDOW du nom de pilote et de la position du pilote. C'est je pense la plus simple de toutes car Tesseract est particuli\u00e8rement bien entrain\u00e9 pour. Cette reconnaissance concerne donc des lettres qui font des mots ou des noms. Voici un exemple de la WINDOW nom de pilote en entr\u00e9e : \"Exemple texte cru\" Ce texte peut paraitre bon, cependant quand on le lance dans Tesseract, il ne va pas toujours donner un r\u00e9sultat parfait. Il faut aussi savoir qu'il y a des noms pas mal plus p\u00e9nibles que Tesseract a plus de mal \u00e0 reconnaitres, soit \u00e0 cause des lettres utilis\u00e9es, soit car le nom est un nom d'une autre r\u00e9gion et qui ne veut rien dire en anglais ce qui emp\u00eache l'utilisation de dictionnaire (Ex : Tsunoda est un nom japonais et parfois il est difficile pour Tesseract de le reconnaitre car si une lettre pose probl\u00eame il ne peut pas trouver de contexte qui puisse l'aider). Donc pour le rendre plus facilement lisible et augmenter les chances que toutes les lettres soient d\u00e9couvertes, voici les \u00e9tapes que j'ai mis en place. 1 : J'inverse les couleurs. Je me suis rendu compte que il \u00e9tait souvent plus facile de trouver un noir sur blanc que blanc sur noir. Je ne suis pas sur que cette \u00e9tape soit capitale cependant. \"Texte invers\u00e9\" 2 : Je fais un Treshhold de 165 car avec moins le texte parfois prend trop du background et avec plus les lettres sont trop fines. \"Texte apr\u00e8s Treshold\" 3 : Je fais un Resize de l'image pour avoir une meilleure r\u00e9solution et permettre une meilleure d\u00e9tection. J'augmente la hauteur et la largeur par un facteur 2. J'ai trouv\u00e9 cette valeur suffisante et aller plus haut consomme beaucoup de ressources. \"Texte apr\u00e8s Resize\" 4: Je fais une tr\u00e8s rapide Dilatation du texte pour retirer le flou amen\u00e9 par la methode de Resize. Je n'utilise qu'une valeur de 1 car je ne veux pas trop changer comment le texte est model\u00e9 je veux juste retirer le flou. \"Texte apr\u00e8s Dilatation\" Explication des methodes pr\u00e9cises plus bas Voila pour ce qui est du post processing. Je ne dis pas que ce sont les meilleurs param\u00eatres possibles mais dans mes tests ce sont ceux qui ont le mieux march\u00e9s. C'est aussi les premi\u00e8res methodes que j'ai pu d\u00e9velopper alors forc\u00e9ment elles n'ont pas le niveau de d\u00e9tails de certaines autres. Mais comme m\u00eame avec ce traitement il n'est pas rare que je me retrouve avec une ou deux lettres pas justes, il faut un moyen d'\u00eatre s\u00fbr que c'est le bon nom qui est trouv\u00e9. Ce qu'il y a de pratique avec les noms de pilotes c'est que on sait d\u00e9ja comment ils s'appellent avant le Grand Prix. En effet dans le fichier de configuration de la reconnaissance, il y a une liste de noms de pilotes. Cela veut dire que au lieu de chercher \u00e0 trouver parfaitement les bonnes lettres, on peut simplement essayer de trouver quel nom de pilote ressemble le plus au nom trouv\u00e9 sur l'image. Pour ce faire j'ai utilis\u00e9 une methode appel\u00e9e la distance de Levenshtein. Pour faire simple c'est une methode qui va calculer les distances de lettres pour determiner entre des strings laquelle ressemble le plus \u00e0 une autre. Pour r\u00e9sumer le fonctionnement dans lordre : On prend l'image on la traite On envoie l'image trait\u00e9e \u00e0 Tesseract On trouve quel nom de pilote ressemble le plus \u00e0 ce r\u00e9sultat On renvoie le nom du pilote Chiffres Cette methode en r\u00e9alit\u00e9 utilise simplement la m\u00eame methode que celle qui va r\u00e9cup\u00e8rer le texte sur une image. Cependant, la, on envoie \u00e0 Tesseract l'information qu'il ne peut trouver que des chiffres sur l'image ce qui lui permet d'\u00eatre beaucoup plus pr\u00e9cis et de ne pas confondre un 9 avec un P ou un 11 avec un H PAR EXEMPLE (non pas que ca me soit arriv\u00e9 tr\u00e8s r\u00e9guli\u00e8rement et que ca me soit rest\u00e9 dans la gorge \u00e9videmment) L'avantage c'est que cette methode ne demande m\u00eame pas de traitement de la donn\u00e9e en sortie de Tesseract. On \u00e9sp\u00e8re simplement que le post traitement aura suffit. TEMPS : Cette methode regroupe la d\u00e9tection de temps au tour. Il y a trois grands types de WINDOW qui sont concern\u00e9es : La WINDOW du temps au tour La WINDOW du retard sur le leader La WINDOW des secteurs La grande diff\u00e9rence ce sont les ordres de grandeur. Les temps au tour sont en g\u00e9n\u00e9ral entre 50 secondes et 2 minutes. Tandis que les secteurs sont entre 20 et 30 secondes alors que le retard sur le leader peut-\u00eatre de plusieurs minutes. Cependant, tous ces temps poss\u00e8dent le m\u00eame type de post-traitement avant d'\u00eatre envoy\u00e9s \u00e0 Tesseract. Voici un exemple de temps au tour avant toute transformation : \"Temps au tour avant traitement\" On peut avoir l'impression que ce texte est tout \u00e0 fait lisible et facile \u00e0 d\u00e9coder surtout quand on le voit de loin comme ca. Cependant, il faut imaginer que ces chiffres font 13 pixels de haut en comptant le flou et comme expliqu\u00e9 plus haut ce flou dans ces echelles est terrible. \"Temps au tour zoom\u00e9\" Si on donne cette image \u00e0 Tesseract, les '3' deviennent des '9', des '9' deviennent des '8', des '2' deviennent eux aussi des '9', le tout parfois inversement et de mani\u00e8re compl\u00eatement impr\u00e9visible. Ca n'est simplement pas utilisable. Cette partie est un peu plus complexe car si la detection n'est pas fiable les chiffres sont simplement inutilisables. Si \u00e0 tout moment un temps au tour de 1:39.106 devient 1:32.108 c'est juste pas possible. Voici donc les \u00e9tapes de post-traitement que j'ai mis en place pour leur d\u00e9tection : 1: J'applique un Treshold de 185 pour enlever les ambiguit\u00e9s d'alisaising et avoir une image en noir et blanc claire. La valeur de 185 est assez \u00e9lev\u00e9e car le but est de vraiment garder uniquement les contours. Comme les chiffres se ressemlent beaucoup plu que les lettres, il faut tenter le plus possible de conserver leur formes sp\u00e9cifiques. Je me suis rendu compte que cette valeur \u00e9tait une de celles qui marchent le mieux. \"Temps au tour apr\u00e8s Treshold\" 2: J'applique un Resize de 2 pour augmenter la r\u00e9solution des chiffres et permettre une meilleure d\u00e9tection. Le but est d'avoir plus de pixels et donc de permettre \u00e0 Tesseract de mieux utiliser ses matrices de convolution. \"Temps au tour apr\u00e8s Resize\" 3: Comme le Resize am\u00e8ne du flou, j'utilise une methode de Dilatation qui me permet de retirer ce flou et de remplir un peu plus certaines parties qui ont \u00e9t\u00e9 un peu laiss\u00e9e par le Resize ; \"Temps au tour apr\u00e8s Dilatation\" 4: Contrairement aux mots plus haut, la rondeur ajout\u00e9e par la dilatation n'est pas vraiment d\u00e9sir\u00e9e. En effet, elle peut rendre confuse certains chiffres et emp\u00eacher Tesseract de bien trouver le chiffre. Alors j'applique une Erosion qui me permet de contrecarrer en partie les rondeurs ajout\u00e9es par la dilatation et retrouver des chiffres bien form\u00e9es. Pour l' Erosion et la Dilatation j'ai utilis\u00e9 une valeur de 1 car je ne voulais pas trop changer les chiffres. \"Temps au tour apr\u00e8s Erosion\" Explication des methodes pr\u00e9cises plus bas Et avec ce post processing on retrouve de plut\u00f4ts bon r\u00e9sultats qui demandent peu de traitement. Le traitement d\u00e9pend du type de WINDOW cependant. Pour les secteurs on indique \u00e0 Tesseract que les caract\u00e8res autoris\u00e9s sont : \"0123456789.\" Pour les temps au tour on autorise plut\u00f4t \"0123456789.:\" Et pour les \u00e9carts on autorise \"0123456789.+\" Ensuite on r\u00e9cup\u00e8re une liste de chiffres qui'il va falloir transformer en milisecondes pour faciliter le stockage et l'envoi. Le programme nettoie un peu la chaine avant de la convertir. Par exemple parfois le ':' de 1:34.456 est compris comme un '1' ou un '2' et il faut faire attention \u00e0 detecter quand ca arriver. Je passe les d\u00e9tails du reste du nettoyage car c'est vraiment du cas par cas mais quand on a finit de nettoyer la chaine on peut transformer les chaines de minutes secondes et milisecondes en un total de milisecondes. Pour r\u00e9sumer le fonctionnement dans l'ordre : On prend l'image et on lui applique une s\u00e9rie de filtres On envoie l'image filtr\u00e9e \u00e0 Tesseract On nettoie le r\u00e9sultat Tesseract pour compenser certains biais On convertis le r\u00e9sultat en milisecondes Pneus La on arrive sur la partie la plus p\u00e9nible. Pour comprendre la probl\u00e9matique il faut d'abord faire un petit point sur comment les pneus fonctionnent en Formule 1. Depuis 2019 en Formule 1 nous avons 5 grandes familles de pneus : Les pneus tendres Les pneus medium Les pneus durs Les pneus interm\u00e9diaires Les pneus pluie \"Gamme de pneus Pirelli\" Les trois premiers pneus sont des pneus faits pour piste s\u00e8che, le pneu interm\u00e9diaire pour piste humide et le neu pluie pour la pluie. Chaque pneu a sa dur\u00e9e de vie et son niveau de performance propre mais je ne vais pas rentrer dans le d\u00e9tail ici. Tout ce qu'il faut savoir ce que savoir sur quel pneu chaque pilote est et depuis combien de temps il les chausse est une information tr\u00e8s importante. Chaque pneu a une couleur donn\u00e9e qui permet de les diff\u00e9rencier. Voici un exemple de ce \u00e0 quoi une WINDOW de pneus peut ressembler : \"Exemple zone pneus 1\" Mais cette zone peut aussi ressembler \u00e0 ca : \"Exemple zone pneus 2\" Mais aussi \u00e0 ca : \"Exemple zone pneus 3\" Voire m\u00eame ca : \"Exemple zone pneus 4\" Je pense que vous pouvez tout de suite comprendre la difficult\u00e9 que repr\u00e9sente la t\u00e2che de r\u00e9cup\u00e8ration de donn\u00e9es \u00e0 partir de cette image. En gros le fonctionnement de cette zone d'information est assez simple. Au fur et \u00e0 mesure que la course avance, le trait fait de m\u00eame. Le chiffre dans le round tout \u00e0 droite indique le nombre de tour que le pilote a pass\u00e9 sur ce pneu. La couleur indique le type de pneu. Si il y a une lettre \u00e0 la place d'un chiffre c'est que c'est le premier tour sur ce pneu. La lettre indique le type de pneu. Et pas besoin de dire que si on essaie simplement de donner l'image \u00e0 Tesseract on ne r\u00e9cup\u00e8re ni les chiffres ni les lettres correctement si ce n'est pas du tout. Il faut donc utiliser une methode qui permette d'isoler le rond le plus \u00e0 droite, lui appliquer un traitement qui permette \u00e0 Tesseract de lire ce qu'il y a marqu\u00e9 et qui puisse determiner quel pneu est en train d'\u00eatre utilis\u00e9. J'ai d\u00e9cid\u00e9 de m'occuper dans un premier temps de trouver ce rond avant d'appliquer les filtres car plus l'image est petite plus les filtres sont rapides. Le programme va tirer un trait depuis le bord droit de la zone, et il va avancer vers la gauche jusqu'\u00e0 trouver un obstacle. Je d\u00e9tecte un obstacle si le pixel sur lequel est mon trait poss\u00e8de une valeur de plus de 0x50 dans le channel R,G ou B. J'ai trouv\u00e9 en faisant des tests que les couleurs de background de la F1TV ne d\u00e9passaient jamais ces valeurs. Ensuite apr\u00e8s avoir trouv\u00e9 le premier obstacle, je r\u00e9cup\u00e8re une zone qui doit englober le cercle. Voici un exemple avec cette image en entr\u00e9e : \"Zone compl\u00eate\" Elle est automatiquement coup\u00e9e de cette facon : \"Zone coup\u00e9e automatiquement\" Cela me permet d'isoler uniquement ce qui m'int\u00e9resse ce qui est tr\u00e8s pratique pour Tesseract et pour la detection de couleur. Ensuite avec cette image je peux commencer le processus de reconnaissance. Je commence par faire une moyenne de tous les pixels de l'image en excluant les pixels trop sombres qui font s\u00fbrement partie du background ou du chiffre. Ensuite j'utilise une methode qui calcule la diff\u00e9rence entre la couleur obbtenue et la liste de couleurs possible. Il y a cinq couleurs des pneus possibles : \"#ff0000\" pneu tendre/soft \"Couleur d'un pneu tendre\" \"#f5bf00\" pneu medium \"Couleur d'un pneu medium\" \"#a4a5a8\" pneu dur/hard \"Couleur d'un pneu dur\" \"#00a42e\" pneu inter \"Couleur d'un pneu interm\u00e9diaire\" \"#2760a6\" pneu pluie/wet \"Couleur d'un pneu pluie\" Ce qui est pratique c'est que m\u00eame dans les cas ou il n'y a pas beaucoup de couleur comme celui la : \"Pneu dur avec 0 tours\" On arrive \u00e0 une couleur moyenne de : \"Couleur moyenne de l'image ci dessus apr\u00e8s soustraction du background\" Et il est donc assez facile de determiner le type de pneu en question. Attention, les r\u00e9sultats peuvent \u00eatre tr\u00e8s vite d\u00e9rang\u00e9s par la couleur du pneu pr\u00e9c\u00e9dent si le d\u00e9coupage de la fen\u00eatre n'a pas \u00e9t\u00e9 assez pr\u00e9cis. Ensuite il \"suffit\" de lire le chiffre dans le rond et si on arrive pas \u00e0 le lire alors c'est que c'est une lettre et on sait que le nombre de tours est donc de 0. Maintenant vient le moment tr\u00e8s sympatique de la lecture du chiffre. Vous saurez que Tesseract en plus de detester les grandes images et les images avec des couleurs, deteste \u00e9galement les formes dans une image. Donc dans notre cas, le round de couleur autour du chiffre, m\u00eame si il n'est pas complet, il interf\u00e8re avec la reconnaissance et emp\u00eache de bien lire le chiffre. Il faut donc retirer le background et ensuite la couleur. Sauf que comme le chiffre est de la couleur du background, si on retire le background et ensuite la couleur il ne reste plus rien. Il faut donc retirer le background AUTOUR du rond, et ensuite si on retire la couleur il devrait rester le chiffre sur fond blanc. Pour se faire, j'ai tir\u00e9 des traits depuis les bords de l'image jusqu'\u00e0 ce qu'ils rencontrent le rond. Ensuite je retire tous les pixels entre le rond et les bords de l'image ce qui nous donne ceci : \"Zone pneu avec le background en moins\" Ensuite on peu retirer les pixels qui ont une valeur dans un channel RGB plus haute qu'un certain seuil : \"Zone avec le reste des couleurs supprimm\u00e9es\" Et la on a ce que l'on veut ! A partir de la c'est les filtres que l'on connait qui sont utilis\u00e9s pour en faire une image plus facile \u00e0 utiliser par Tesseract. 1 : On effectue un Resize de facteur 4 (oui c'est beaucoup mais en m\u00eame temps le chiffre est vraiment petit \u00e0 la base) qui permet d'avoir une image d'une bien meilleure r\u00e9solution. \"Filtre 1\" 2: On fait une Dilatation de facteur 1 pour retirer tout le flou de l'image pour aider Tesseract \"Resultat\" Et on a un chiffre qui est utilisable par Tesseract ! Explication des methodes pr\u00e9cises plus bas Pour r\u00e9sumer : On prend l'image de la zone et on la crop pour ne garder que la partie essentielle On d\u00e9termine le type de pneu avec la couleur moyenne de la zone On retire le background autour de cette zone On retire la couleur qui reste pour ne garder que le chiffre On augmente la r\u00e9solution du chiffre On rend ce chiffre net On envoie l'image trait\u00e9e et filtr\u00e9e \u00e0 Tesseract On d\u00e9termine le nombre de tours que le pilote a fait avec ses pneus avec le r\u00e9sultat de Tesseract DRS Bon ca c'\u00e9tait plut\u00f4t simple j'ai simplement v\u00e9rifi\u00e9 si la moyenne de vert d\u00e9passait une certaine valeur et puis voila. Filtres et methodes sur les images Dans ce projet on a du utiliser diff\u00e9rentes methodes d'\u00e9dition d'image que ce soit sous forme de filtres ou de modification de l'image directement. Voici un sommaire des methodes utilis\u00e9es et comment elles fonctionnent. Tresholding Cette methode sert \u00e0 passer d'une image en couleurs \u00e0 une image binaire noir blanc. C'est une \u00e9tape tr\u00e8s importante pour l'OCR car elle permet (si bien faite) d'isoler du texte de son background. Un exemple ici : \"Exemple treshold\" Le fonctionnement est assez simple mais il peut \u00eatre fait de diff\u00e9rentes mani\u00e8res mais dans mon cas voici comment l'algorythme fonctionne sachant qu'il demande en entr\u00e9e la Bitmap que l'on veut modifier ainsi que la valeur de Treshold : On parcours chaque pixel de l'image On convertir la couleur du pixel en une valeur de gris pour avoir la m\u00eame valeur en R,G et B (Formule utilis\u00e9e : grey = R x 0.3 + G x 0.59 + B x 0.11) Si le r\u00e9sultat de la valeur de gris est au dessus de la valeur de treshold, le pixel est pass\u00e9 en blanc complet et dans le cas contraire il est pass\u00e9 en noir complet On retourne la Bitmap modifi\u00e9e Un algorythme pas forc\u00e9ment complexe mais qui peut augmenter de mani\u00e8re titanesque les chances de r\u00e9ussir une OCR Resize Cette methode sert \u00e0 augmenter la r\u00e9solution d'une image pour am\u00e9liorer la pr\u00e9cision de l'algorythme de Tesseract. En effet, avec trop peu de pixels, la matrice de convolution n'est pas toujours aussi efficace. Il ne faut pas confondre cette methode d'augmentation de la taille avec une simple interpolation. En effet une augmentation de taille interpol\u00e9e ne vas pas vraiment changer la r\u00e9solution, l'image sera toujours aussi pixelis\u00e9e, seulement, les pixels seront compos\u00e9es de plus de pixels comme dans l'exemple ci dessous : \"Exemple d'interpolation lin\u00e9aire\" Dans mon projet j'utilise de l'interpolation bicubique qui va cr\u00e9er de l'information pour tenter de combler le vide et produire une image r\u00e9ellement plus grande et avec plus de details mais en ajoutant du flou. \"Exemple des diff\u00e9rents types d'interpolation\" Le but est d'aller chercher dans les pixels alentours les couleurs qui sont d\u00e9ja pr\u00e9sente et de jouer avec des poids pour tenter de faire une pr\u00e9diction de ce que ce pixel aurait \u00e9t\u00e9 si l'image avait plus de detail. Voici un exemple assez parlant : \"Exemple interpolation bicubique (avant)\" \"Exemple interpolation bicubique (apr\u00e8s)\" On pourrait croire que c'est inutile mais dans le contexte de Tesseract ajouter des d\u00e9tails pour tenter de simuler une meilleure r\u00e9solution m\u00eame en cr\u00e9ant du flou est int\u00e9ressant pour mieux remplir la matrice de convolution. Mais il est possible de r\u00e9duire ce flou avec d'autres m\u00e9thodes \u00e9galement. (Dans mon code je n'ai pas utilis\u00e9 du code fait main mais j'utilise une librairie qui me permet de le faire) Il faut simplement faire attention car c'est un proc\u00e9d\u00e9 assez lourd en performances. Dilatation et Erosion Cette methode et la suivante font partie des methodes de transformation morphologiques. Ces methodes sont utilis\u00e9es pour accentuer les formes et les epaissir ou les r\u00e9duire et les affiner. Elles poss\u00e8dent l'aventage \u00e9galement de retirer le flou d'une image ce qui est tr\u00e8s pratique si utilis\u00e9 apr\u00e8s l'utilisation de methodes comme Resize . Je ne vais pas trop rentrer dans les d\u00e9tails de ces methodes car leur fonctionnement est un peu plus lourd en math si on veut faire une v\u00e9ritable explication du pourquoi et du comment ca marche aussi bien. Pour notre projet je dirais que l'important est de savoir que ce sont deux outils tr\u00e8s pratiques pour changer la morphologie des lettres et des chiffres et qu'on peut les utiliser pour corriger du flou et/ou des art\u00e9facts apparus lors de la binarisation de l'image ou de la suppression de fond. Remove Background Cette methode est assez simple et est juste une methode qui va passer en revue tous les pixels de l'image et si la couleur d'un pixel s'apparente \u00e0 celle d'un pixel de fond il est pass\u00e9 en noir total ou en blanc total. Le but est de permettre au reste du programme de fonctionner avec des couleurs moins ambigues. Une variante sp\u00e9cialis\u00e9e pour la reconnaissance des pneus appel\u00e9e affectueusement Remove Useless cherche \u00e0 atteindre le m\u00eame bu mais est bien plus soffistiqu\u00e9e et sp\u00e9cialis\u00e9e pour retirer le background autour d'un cercle de couleur pour ensuite retirer la couleur et qu'il ne reste qu'un chiffre. Pour plus de details voir la detection de pneus. Il y aussi d'autre methodes comme un filtre Gaussien ou Highlight countour que j'ai du d\u00e9velopper mais que je n'ai pas utilis\u00e9 donc je ne vais pas en parler ici. Interpr\u00e9tation des donn\u00e9es Stockage des donn\u00e9es Affichage des donn\u00e9es Pr\u00e9dictions Tests [A remplir au fur et \u00e0 mesure de la cr\u00e9ation des tests] R\u00e9sum\u00e9 des difficult\u00e9s techniques [A remplir au fur et \u00e0 mesure dans la seconde moiti\u00e9 du travail de dipl\u00f4me] Optimisation du programme [A remplir \u00e0 la fin du projet pour parler des diff\u00e9rentes methodes d'optimisation] Ethique du projet [A remplir \u00e0 la fin du projet pour parler des questions ethiques du projet (Ex: Utilisation potentiellement abusive de la F1Tv ou L'histoire des cookies)] Am\u00e9liorations futures [A remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me] Conclusion [A remplir la derni\u00e8re semaine du travail de dipl\u00f4me]","title":"Rapport Track Trends V1.0"},{"location":"index.html#rapport-track-trends-v10","text":"Rohmer Maxime Travail de dipl\u00f4me Technicien ES 2023","title":"Rapport Track Trends V1.0"},{"location":"index.html#introduction","text":"","title":"Introduction"},{"location":"index.html#resume","text":"Track Trends est un outil de r\u00e9cup\u00e9ration et d'analyse de donn\u00e9es de courses de Formule 1. Pour le contexte, en dehors des cours, j'exerce diff\u00e9rentes activit\u00e9s dont celle de Live Ticker F1 pour le 20 minutes. Pour m'aider dans ce travail, j'utilise actuellement la F1TV \u00e0 laquelle je suis abonn\u00e9 qui me propose non seulement un feed vid\u00e9o de meilleure qualit\u00e9 avec des commentaires plus pertinents que ceux de la RTS mais qui aussi me permet d'acc\u00e9der \u00e0 un feed vid\u00e9o tr\u00e8s important : la chaine data. Ce dernier ressemble \u00e0 cela : \"Screenshot du feed data de la f1tv\" (Attention ce n'est pas un joli tableau HTML, mais bien une vid\u00e9o qui contient un tableau.) Sauf que toutes les informations sont \u00e9tal\u00e9es p\u00eale-m\u00eale sans hi\u00e9rarchie ce qui fait que cela me prendrait trop de temps de tout d\u00e9chiffrer \u00e0 chaque fois, ce qui me fait rater des choses int\u00e9ressantes. Le but du projet est donc de fournir un outil qui hi\u00e9rarchise et affiche diff\u00e9remment les donn\u00e9es pour faciliter leur lecture et me permettre de faire de meilleurs commentaires.","title":"R\u00e9sum\u00e9"},{"location":"index.html#abstract","text":"Track Trends is a Formula 1 data is a tool that displays and interpret data. To understand everything,first ,a little bit of context. In my free time I have multiple activities and one is to be the Live Ticker F1 for the local journal \"20 minutes\" (Owned by Tamedia). to help me in this work I'm currently using the F1TV to which I'm currently subscribed because it provides me with a better video feed with better commentary than the ones from the RTS (in my opinion) but also because it gives me access to a very important video feed : the data channel You can see in the chapter above an example of the F1TV DATA CHANNEL. [Note : Even tough it looks like a pretty HTML table on wich you could easely get infos... Its not. Its a video feed] You can see a lot of data all well and good BUT! All the data is displayed the same in a big table which make it really hard to read totally in a hurry, which means that I miss a lot of useful information. The point of the project then is to provide a tool that can display those data by taking into account their relevance. So for example a driver that is 10s away from everyone and that is doing some normal lapTimes will be less displayed or even not displayed at all so I can focus on the drivers that are battling each others. This tool would help me not miss the battles and details that are happening in the back and therefore not being broadcasted on TV.And it could be a usefull tool for anyone who wants a better insight of how the race is going by looking at the data. This kind of project already exists in the form of the AWS tool \"F1 Insight\" but it is not avaible to the public. We can only see some of its predictions (that are trash) and data dumps in the live feed when the TV directors feel like it.","title":"Abstract"},{"location":"index.html#description-du-besoin","text":"Comme expliqu\u00e9 dans le r\u00e9sum\u00e9, je suis Live Ticker F1. Mais pour mieux comprendre le besoin que j'ai, je pense qu'il est pertinent de comprendre comment je travaille. Pendant un Grand Prix de Formule 1 j'ai plusieurs t\u00e2ches \u00e0 effectuer : Suivre les diff\u00e9rents \u00e9v\u00e8nements du Grand Prix Changer le titre et la photo de titre du Live Chercher des Tweets ou des Images \u00e0 int\u00e9grer Ecrire les commentaires en faisant attention \u00e0 dire ce qu'il se passe mais aussi l\u2019expliquer, ce que cela implique, mais aussi ce que cela veut dire pour le reste de la course. Comprendre et expliquer les strat\u00e9gies Tout cela toutes les cinq minutes max... Cela veut dire que je dois \u00eatre le plus rapide possible quand je cherche des informations. Et comme le tableau en comporte trop et bien, je suis oblig\u00e9 de le lire en diagonale. Par exemple dans le tableau, les infos que je cherche le plus sont : Le nombre de places gagn\u00e9es (surtout au d\u00e9part) Les \u00e9carts entre les pilotes (surtout ceux qui sont en dessous de deux secondes). Les pneus de chaque pilote et combien de tours, ils ont fait dessus Les temps d'arr\u00eats aux stands Les temps au tour (surtout pour la strat\u00e9gie) Mais pleins d'autres informations existent si on les recoupe sur plusieurs tours. Un outil qui permettrait de mettre en \u00e9vidence les informations importantes serait donc une tr\u00e8s grosse plus-value pour mon travail et s'il est facile \u00e0 installer et \u00e0 utiliser, il se pourrait qu'il devienne indispensable.","title":"Description du besoin"},{"location":"index.html#cahier-des-charges","text":"Il s'agit d'une version coup\u00e9e du cahier des charges qui ne contient pas l'explication du contexte. Mais l'original est disponible sur ce site \u00e9galement. Il est toutefois normal d'y voir des choses r\u00e9p\u00e9t\u00e9es ou l\u00e9g\u00e8rement diff\u00e9rentes, en effet, il n'a pas \u00e9t\u00e9 \u00e9crit en m\u00eame temps que le reste de ce document.","title":"Cahier des charges"},{"location":"index.html#projet","text":"Un outil de style compagnon sous forme d'application C# Windows Form qui r\u00e9cup\u00e8re en temps r\u00e9el les informations de la course et affiche les informations les plus importantes. Le but est non seulement de faciliter mon job, mais aussi faire en sorte d'am\u00e9liorer la plus-value de mon travail en me permettant de fournir des commentaires qui ne sont pas disponibles pour le tout venant qui regarde simplement le flux RTS. Exemples : Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand Maintenant afficher diff\u00e9remment les infos, c'est sympa, mais cela serait encore mieux de traiter ces data et de permettre des petites pr\u00e9dictions. Exemples : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents","title":"Projet"},{"location":"index.html#realisation","text":"Malheureusement, la Formula 1 Management ne propose aucune API publique qui puisse nous permettre de faire ce projet \"simplement\". La raison la plus probable \u00e9tant qu'Amazon avec son service AWS propose exactement ce genre de services pour le flux t\u00e9l\u00e9vis\u00e9 et il doit y avoir un contrat d'exclusivit\u00e9. Il existe des API \"Pirates\" faites par la communaut\u00e9, le probl\u00e8me est qu'elles ne sont pas forc\u00e9ment des plus pratiques \u00e0 utiliser. Mais comme je poss\u00e8de un abonnement premium ++ \u00e0 la F1TV, j'ai acc\u00e8s pour chaque grand prix \u00e0 un flux vid\u00e9o nomm\u00e9 : DATA F1 CHANNEL Qui ressemble \u00e0 \u00e7a : \"Exemple de la Data Channel\" Donc la seule fa\u00e7on que je vois de r\u00e9cup\u00e9rer ces donn\u00e9es est de les prendre directement sur ce feed. M\u00eame si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout \u00eatre la r\u00e9cup\u00e9ration des donn\u00e9es et leur stockage. Les donn\u00e9es viennent du flux vid\u00e9o et ainsi dans un premier temps, il va falloir r\u00e9cup\u00e9rer d'une mani\u00e8re ou d'une autre des images qui viennent d'un grand prix en direct ou en rediffusion. Ensuite, dans un second temps, il faut lire les informations directement sur l'image en utilisant une librairie pr\u00e9vue pour (exemple Tesseract) et v\u00e9rifier l'int\u00e9grit\u00e9 de ces derni\u00e8res pour qu'on puisse ensuite les stocker. Dans un troisi\u00e8me temps, il faut stocker toutes ces donn\u00e9es dans une forme qui permette d'aller facilement faire des requ\u00eates de r\u00e9cup\u00e9ration et d\u00e9j\u00e0 pr\u00e9parer des m\u00e9thodes qui permettent de r\u00e9cup\u00e9rer des infos importantes (ex : la moyenne des cinq derniers tours, le temps moyen d'arr\u00eat etc.) pour faciliter la derni\u00e8re \u00e9tape Quand tout cela est fait, on peut ensuite s'amuser un peu avec les Data. La derni\u00e8re \u00e9tape est donc l'affichage. L'id\u00e9e est de cr\u00e9er une Windows Form qui contienne toutes ces informations dans un format beaucoup plus lisible et avec laquelle on pourrait interagir pour permettre de plus facilement commenter les Grands Prix. (exemple plus bas avec un croquis de ce \u00e0 quoi l'application pourrait ressembler) Voici la liste des donn\u00e9es qui pourraient \u00eatre affich\u00e9es (Non contractuel, simplement des id\u00e9es). Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand La moyenne de temps que les pilotes perdent dans les stands La performance moyenne des 5 types de pneus La moyenne de temps de chaque pilote sur le pneu actuel Le nombre de points que chaque pilote gagnerait selon sa position Le classement de la course Voire m\u00eame si c'est possible : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents Pr\u00e9dire les temps au tour de chaque pilote selon l'usure des pneus Voici un exemple d'interface possible pour une page : \"Protype de l'app fait sur Figma\"","title":"R\u00e9alisation"},{"location":"index.html#cas-dutilisation","text":"'*'On va consid\u00e9rer que tous les user ont un abonnement F1 TV PRO Un user veut r\u00e9cup\u00e9rer les data : Il ouvre son navigateur et lance la page DATA de la F1 TV Il calibre la capture des data via le programme (pour la premi\u00e8re utilisation). Il confirme que les donn\u00e9es initiales sont bonnes (pour la premi\u00e8re utilisation). Il regarde tranquille son Grand Prix Le programme r\u00e9cup\u00e8re les data : Il r\u00e9cup\u00e8re des images depuis la F1TV Il utilise Tesseract (ou autre) pour en r\u00e9cup\u00e9rer les infos. Il met ces infos dans un Objet Pilote, dans un Objet course avec un attribut tour pour hi\u00e9rarchiser les data Pour ce qui est de l'affichage, l'id\u00e9e est de faire une application C# comme on l'a appris \u00e0 l'\u00e9cole, mais avec assez de style pour qu'elle puisse \u00eatre agr\u00e9able \u00e0 utiliser. Quand le programme affiche les data : Il prend les donn\u00e9es venant directement de la F1TV. Il affiche diff\u00e9remment les donn\u00e9es pour permettre une meilleure lisibilit\u00e9 Il interpr\u00e8te avec des r\u00e8gles plut\u00f4t simples certaines data pour faire des minipr\u00e9dictions ou aider \u00e0 la lecture Il r\u00e9cup\u00e8re des infos d'autres courses pour les comparer et proposer des pr\u00e9dictions plus int\u00e9ressantes","title":"Cas d'utilisation"},{"location":"index.html#difficultes-techniques","text":"R\u00e9cup\u00e9rer un flux vid\u00e9o plut\u00f4t propre malgr\u00e9 les contres mesures de la F1 TV pour en emp\u00eacher la lecture par un logiciel Si on doit passer par une capture d'\u00e9cran, trouver un moyen de stocker les donn\u00e9es de mani\u00e8re \u00e0 pr\u00e9voir que parfois un tour pourrait avoir plus de donn\u00e9es qu'un autre, que le user peut mettre pause, ou m\u00eame qu\u2019il revienne en arri\u00e8re. D\u00e9velopper des algorithmes pour r\u00e9cup\u00e9rer les donn\u00e9es comme les diff\u00e9rents pneus utilis\u00e9s ou l'activation du DRS ainsi que d\u00e9velopper des moyens de nettoyer les r\u00e9sultats de l'OCR (Par exemple utiliser diff\u00e9rents champs redondants pour comparer les r\u00e9sultats) Stocker les donn\u00e9es sur une base pour les traiter plus tard tout en pr\u00e9voyant un moyen de voir les stats live D\u00e9velopper des algorithmes de pr\u00e9diction qui prennent en compte d'anciennes courses pour tenter de pr\u00e9dire des choses comme les arr\u00eats aux stands par exemple.","title":"Difficult\u00e9s techniques"},{"location":"index.html#differences-sur-le-cahier-des-charges","text":"[\u00c0 remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me]","title":"Diff\u00e9rences sur le cahier des charges"},{"location":"index.html#planning-previsionnel","text":"Mes suiveurs m'ont demand\u00e9 un planning de type GANTT pour ce travail de dipl\u00f4me Je n'ai pas utilis\u00e9 un logiciel particulier pour faire ce dernier, mais je me suis inspir\u00e9 des principes fondamentaux d'un diagramme de ce type. Comme l'original a \u00e9t\u00e9 fait sur Excel, je ne peux pas vraiment l'ins\u00e9rer de mani\u00e8re lisible ici, mais il est disponible dans le dossier Planning. Mais voici un r\u00e9sum\u00e9 de son contenu :","title":"Planning pr\u00e9visionnel"},{"location":"index.html#taches","text":"J'ai d\u00e9cid\u00e9 de d\u00e9composer mon planning en trois grands types de t\u00e2ches. Programmation Documentation Tests L'id\u00e9e est de permettre une meilleure lisibilit\u00e9 et de me permettre \u00e0 moi de me faire plus facilement \u00e0 l'id\u00e9e de ce qu'il m'attend. Voici la liste des t\u00e2ches par rubrique :","title":"T\u00e2ches"},{"location":"index.html#pt","text":"Cette rubrique contient les t\u00e2ches qui n'ont pas leur place dans les trois cat\u00e9gories principales.","title":"PT"},{"location":"index.html#pt1-preparation-au-travail-de-diplome-2","text":"Cette t\u00e2che est un peu hors cat\u00e9gorie, mais c'est normal, c'est une supert\u00e2che qui regroupe beaucoup de choses. C'est une t\u00e2che qui est planifi\u00e9e pour deux jours et qui normalement devrait \u00eatre faite les deux premiers jours du travail. Le but est de pr\u00e9parer tout ce qui peut \u00eatre pr\u00e9par\u00e9 en avance niveau documentation et mise en place pour ne pas avoir besoin de s'en soucier ensuite.","title":"PT1 / pr\u00e9paration au travail de dipl\u00f4me (2)"},{"location":"index.html#dt","text":"Rubrique documentation qui contient toutes les t\u00e2ches en rapport de pr\u00e8s ou de loin avec la documentation du projet.","title":"DT"},{"location":"index.html#dt1-creation-du-poster-1","text":"Cette t\u00e2che consiste \u00e0 faire une version num\u00e9rique du poster qui soit en accord avec les consignes qu'on nous a donn\u00e9es. Le but est aussi et surtout de faire poster dont je sois fier et que je sois content de montrer. Il y a d\u00e9j\u00e0 des croquis de poster et j'ai clairement pr\u00e9vu de travailler sur \u00e7a pendant les vacances alors, je n'ai mis qu'un jour et je l'ai plac\u00e9 juste avant le rendu de ce dernier.","title":"DT1 Cr\u00e9ation du poster (1)"},{"location":"index.html#dt2-documentation-analyse-de-lexistant-2","text":"Cette t\u00e2che est d\u00e9di\u00e9e \u00e0 l'\u00e9criture de la documentation et plus pr\u00e9cis\u00e9ment de l'analyse de l'existant. Comme il y a pas mal de technologies utilis\u00e9es dans mon projet, j'aimerais faire correctement un vrai debrief de pourquoi j'ai utilis\u00e9 l'une ou l'autre alors, j'ai assign\u00e9 deux jours dessus.","title":"DT2 Documentation Analyse de l'existant (2)"},{"location":"index.html#dt3-documentation-analyse-organique-5","text":"Cette t\u00e2che est la plus grosse dans la cat\u00e9gorie documentation. Il s'agit de documenter comment l'application fonctionne. J'y ai mis cinq jours et je pense que c'est un minimum car c'est dans cette t\u00e2che que je vais devoir d\u00e9tailler exactement comment fonctionne chaque partie du projet. Ces cinq jours sont \u00e9parpill\u00e9s sur le projet en g\u00e9n\u00e9ral \u00e0 la fin du d\u00e9veloppement de chaque grande partie de projet. Le but est de ne rien oublier et de ne pas avoir \u00e0 tout faire en m\u00eame temps.","title":"DT3 Documentation Analyse organique (5)"},{"location":"index.html#dt4-documentation-analyse-fonctionnelle-2","text":"Cette t\u00e2che est d\u00e9j\u00e0 moins grosse, elle consiste \u00e0 documenter le fonctionnement de l'application et comment utiliser les composants que j'ai d\u00e9velopp\u00e9s. Je l'ai mis en fin de projet, car comme j'ai l'habitude de faire des analyses fonctionnelles plut\u00f4t pr\u00e9cises, le moindre changement dans l'UI peut tout rendre obsol\u00e8te. J'y ai mis deux jours, car j'aimerais correctement documenter avec de bonnes photos et sc\u00e9narios pour qu'on puisse voir toutes les possibilit\u00e9s de l'application.","title":"DT4 Documentation Analyse fonctionnelle (2)"},{"location":"index.html#dt5-documentation-tests-1","text":"Cette t\u00e2che est un peu plus petite qu'elle ne le devrait. Elle concerne la documentation des diff\u00e9rents tests. Je n'y ai mis qu'un seul jour, car en r\u00e9alit\u00e9 les diff\u00e9rentes t\u00e2ches de tests contiennent aussi beaucoup de documentation,","title":"DT5 Documentation Tests (1)"},{"location":"index.html#dt6-documentation-reste-2","text":"Cette t\u00e2che est une t\u00e2che un peu vague. Elle contient toutes les actions autres que j'aurai besoin de faire (Mise au propre, orthographe, g\u00e9n\u00e9ration de PDF ...). J'y ai mis deux jours pour avoir un peu de marge, car ce sont toujours des t\u00e2ches qui paraissent faciles, mais qui \u00e0 la fin prennent beaucoup de temps si on les fait bien.","title":"DT6 Documentation Reste (2)"},{"location":"index.html#pt_1","text":"Rubrique programmation qui contient toutes les t\u00e2ches qui touchent \u00e0 la programmation et au d\u00e9veloppement de l'application.","title":"PT"},{"location":"index.html#pt1-programmation-recuperation-des-images-3","text":"Cette t\u00e2che est estim\u00e9e \u00e0 seulement trois jours, il ne faut pas s'y m\u00e9prendre, c'est une des t\u00e2ches les plus dures et lourdes niveaux documentation en explications. Cependant, un POC (Proof Of Concept) assez avanc\u00e9 a d\u00e9j\u00e0 \u00e9t\u00e9 fait et donc cela permet de n'envisager que trois jours, car il suffit de l'impl\u00e9menter et de la paufinner. Cette t\u00e2che consiste \u00e0 prendre en entr\u00e9e un lien de Grand Prix et de sortir une image tous les x secondes de la page DATA. Cela peut sembler simple, mais pour le faire sans prendre d'espace d'\u00e9cran et ne demandant pas \u00e0 l'utilisateur de copier-coller quoi que ce soit o\u00f9 de donner ses identifiants F1TV c'est un challenge. Cela peut paraitre curieux alors de mettre cette t\u00e2che loin dans le planning m\u00eame si c'est la premi\u00e8re \u00e9tape du projet. Encore une fois cela s'explique avec le fait qu'il y a d\u00e9j\u00e0 un POC qui fonctionne \u00e0 peu pr\u00e8s et que donc pr\u00e9f\u00e8re commencer avec des t\u00e2ches plus incertaines dans le cas o\u00f9 elles prendraient plus de temps que pr\u00e9vu.","title":"PT1 Programmation r\u00e9cup\u00e9ration des images (3)"},{"location":"index.html#pt2-programmation-ocr-5","text":"Cette t\u00e2che consiste \u00e0 d\u00e9velopper la partie qui reconnait le texte sur les images. C'est tr\u00e8s certainement la t\u00e2che qui risque le plus de d\u00e9border car c'est celle qui est la plus complexe techniquement puisqu'elle demande non seulement la lecture sur image, mais aussi le d\u00e9veloppement d'algorithmes de traitement de cette donn\u00e9e pour \u00eatre s\u00fbr qu'elle a bien \u00e9t\u00e9 lue. J'y ai ainsi allou\u00e9 cinq jours, mais j'esp\u00e8re que j'arriverai \u00e0 gagner du temps sur les autres pour y allouer plus dans le planning effectif, car je suis convaincu que plus, on y passe du temps, meilleur sera le r\u00e9sultat.","title":"PT2 Programmation OCR (5)"},{"location":"index.html#pt3-programmation-stockage-et-modele-5","text":"Cette partie est moins technique, mais concerne le stockage des donn\u00e9es que nous retourne la lecture des images. Mais elle va demander de la r\u00e9flexion et de l'intelligence de programmation, car il faut que cette partie anticipe les besoins de la vue et pr\u00e9pare un terrain fertile qui ne demande pas un refactor quand on passera au d\u00e9veloppement de la vue. C'est pour cela que je lui ai aussi assign\u00e9 cinq jours de travail et elle doit absolument \u00eatre commenc\u00e9e apr\u00e8s la lecture.","title":"PT3 Programmation, stockage et mod\u00e8le (5)"},{"location":"index.html#pt4-programmation-vue-de-lapp-5","text":"Cette partie peut \u00eatre horrible comme tr\u00e8s facile, cela d\u00e9pend compl\u00e8tement de la qualit\u00e9 du travail avant. Si le mod\u00e8le est parfait et que les donn\u00e9es sont int\u00e8gres, cela devrait \u00eatre plut\u00f4t simple de les afficher de mani\u00e8re int\u00e9ressante. Cependant, cette partie d\u00e9bordera s\u00fbrement un peu, car tout le temps gagn\u00e9 avec de bonnes donn\u00e9es sera utilis\u00e9 pour tenter de faire de la pr\u00e9diction. Pour ces raisons, je lui ai assign\u00e9 \u00e9galement cinq jours de travail et elle doit absolument \u00eatre faite apr\u00e8s le mod\u00e8le.","title":"PT4 Programmation Vue de l'APP (5)"},{"location":"index.html#pt5-programmation-mise-en-commun-3","text":"Cette t\u00e2che est aussi un petit peu sp\u00e9ciale, car elle regroupe plusieurs choses. En gros, chaque partie de programmation sera s\u00fbrement assez ind\u00e9pendante et il faudra \u00e0 un moment faire un seul projet C# qui contient tout. Il est difficile d'estimer \u00e0 quel point cela va \u00eatre compliqu\u00e9 alors, j'ai \u00e9t\u00e9 conservateur et j'ai mis trois jours.","title":"PT5 Programmation mise en commun (3)"},{"location":"index.html#tt","text":"Cette rubrique contient les t\u00e2ches qui sont uniquement des tests. La plupart des t\u00e2ches de programmations contiennent d\u00e9j\u00e0 des tests, mais certaines demandent une attention particuli\u00e8re.","title":"TT"},{"location":"index.html#tt1-tests-ocr-2","text":"Cette t\u00e2che est une des t\u00e2ches les plus importantes. Son but est de faire un protocole de tests complet qui permette de comparer les diff\u00e9rents algorithmes de reconnaissance de texte. Je l'ai mise apr\u00e8s la reconnaissance, mais m\u00eame maintenant en \u00e9crivant ces lignes, je me dis que dans le planning effectif, elle sera faite pendant la t\u00e2che de programmation. En effet, comment savoir si mon tout nouvel algorithme est r\u00e9ellement mieux que le pr\u00e9c\u00e9dent. Je pr\u00e9vois deux jours, car je pense que faire le dataset va prendre beaucoup de temps, il faut pr\u00e9voir un certain nombre d'images et de texte qui pourront ensuite \u00eatre donn\u00e9es sous forme de tests. C'est certes une t\u00e2che de test, mais c'est aussi de la programmation.","title":"TT1 Tests OCR (2)"},{"location":"index.html#tt2-tests-finaux-2","text":"Cette t\u00e2che de tests est un peu vague, elle regroupe les diff\u00e9rents tests pour v\u00e9rifier que les donn\u00e9es sont bien affich\u00e9es correctement. Ce qui serait cool si j'ai du temps en fin de travail de dipl\u00f4me serait de faire un syst\u00e8me de test qui permet d'entrainer le programme \u00e0 mieux reconnaitre certaines choses comme des arr\u00eats aux stands si on lui donne les trois derni\u00e8res ann\u00e9es de grands Prix. J'ai mis une dur\u00e9e arbitraire de deux jours, mais je ne sais pas vraiment combien de temps cela va vraiment durer. Elle est \u00e9videmment \u00e0 effectuer une fois que tout est \u00e0 peu pr\u00e8s termin\u00e9.","title":"TT2 Tests finaux (2)"},{"location":"index.html#planning-effectif-et-differences","text":"[A remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me]","title":"Planning effectif et diff\u00e9rences"},{"location":"index.html#analyse-fonctionnelle","text":"[A remplir au fur et \u00e0 mesure dans la seconde moiti\u00e9 du travail de dipl\u00f4me]","title":"Analyse fonctionnelle"},{"location":"index.html#analyse-organique","text":"[A remplir au fur et \u00e0 mesure que les features sont d\u00e9velopp\u00e9es]","title":"Analyse Organique"},{"location":"index.html#recuperation-des-images","text":"Voici la premi\u00e8re grande \u00e9tape du projet. Pour rappel, Amazon h\u00e9berge directement le site de la F1TV et poss\u00e8de les droits sur les donn\u00e9es de la F1. C'est sous le nom de AWS (le service d'h\u00e9bergement d'Amazon) que la firme apparait en tant que sponsor. On peut voir ce nom appara\u00eetre assez souvent quand on regarde un Grand Prix car comme ils ont la main-mise sur les donn\u00e9es ils peuvent ins\u00e8rer des bandeaux d'informations sur le flux public sur ce qu'il se passe voir m\u00eame faire des pr\u00e9dictions (Bien qu'un peu bancales) \"Exemple insertion AWS en GP\" Ce service s'appelle F1 Insights (Oui c'est un meilleur nom de projet que F1 Companion mais bon) et c'est, je pense, la raison pour laquelle on ne voit aucune API publique qui permette de correctement se renseigner en don\u00e9es en direct pendant un Grand Prix. Ils ont du d\u00e9gotter un juteux contrat pour s'occuper de toute l'infrastructure digitale de la F1 (du moins publique) en \u00e9change d'une exclusivit\u00e9 totale sur certaines choses comme les Data \"Exemple data d'AWS\" Evidemment je ne fais que conjecturer et ce que j'ai dit n'est pas \u00e0 prendre au pied de la lettre mais c'est une explication possible je pense de pourquoi il est si difficile de trouver des donn\u00e9es sur la F1 facilement en temps r\u00e9el. Il existe bien quelques API un peu bancales publiques, mais le probl\u00e8me c'est qu'elles ne sont vraiment pas suffisante et je ne peux pas leur faire confiance quand je commente. Ce qu'il m'aurait fallut c'est une API publique et officielle qui me permette d'\u00eatre sur que les donn\u00e9es sont les bonnes et qu'elles arrivent le plus vite possible. On pourrait croire que c'est impossible car cela n'existe pas comme je l'ai dit MAIS ! Ce n'est pas compl\u00eatement vrai. En effet depuis que je poss\u00e8de un abonnement \u00e0 la F1TV, il existe une source d'informations tr\u00e8s pr\u00e9cieuse qui m'aide \u00e9norm\u00e9ment dans mon quotidien de commentateur de Formule 1. La \"DATA CHANNEL\". La Data Channel est une page de la F1TV qui permet, pour chaque Grand Prix, de visualiser, sous la forme d'un flux vid\u00e9o, diff\u00e9rentes informations capitales sur la course. \"Exemple de Data Channel\" Le probl\u00e8me, c'est que comme je viens de le dire, ces donn\u00e9es ne sont pas accessibles comme un tableau HTML ou un flux RSS ou un tableau JSON. C'est un flux vid\u00e9o. Il faut savoir qu'entretenir une diffusion de flux vid\u00e9o en 1080P pendant deux heures accessible par des milliers d'abonn\u00e9s est EXTR\u00caMENT cher surtout quand on le compare \u00e0 simplement afficher les donn\u00e9es dans un tableau. Ce qui veut dire que ce choix est d\u00e9lib\u00e9r\u00e9 et a un sens au niveau \u00e9conomique. Je pense donc que c'est justement pour \u00e9viter que des petits malins puissent simplement venir scraper l'int\u00e9gralit\u00e9 des donn\u00e9es qu'ils proposent et fasse sa propre API. (C'est d'ailleurs un des sites avec la meilleure protection anti bot du monde) MAIS ce n'est pas par ce que les donn\u00e9es ne sont pas facile \u00e0 avoir qu'elles sont IMPOSSIBLE \u00e0 avoir. Et c'est la que ce projet entre en jeu. Mais pour d\u00e9coder les donn\u00e9es d'une image il faut dabord ... (roulement de tambours) ... Avoir des images ! Et c'est la que vient se glisser cette partie du projet.","title":"R\u00e9cup\u00e9ration des images"},{"location":"index.html#comment-faire","text":"Le but de ce segment est de se concentrer sur la r\u00e9cup\u00e8ration et la mise \u00e0 disposition pour le reste du programme, des images en direct de la F1TV dans la meilleure qualit\u00e9 possible et dans les meilleurs d\u00e9lais. Pour ce faire il y a plusieurs solutions : Reverse engeneer la F1TV pour acc\u00e8der directement au flux sans passer par la plateforme internet et pouvoir prendres images \u00e0 volont\u00e9. Avoir tout simplement une page de la F1TV ouverte sur un \u00e9cran et prendres des screenshots \u00e0 intervals r\u00e9guliers. Simuler un navigateur internet sans qu'il soit affich\u00e9 et le contr\u00f4ler automatiquement pour qu'il prenne des captures. La premi\u00e8re option aurait \u00e9t\u00e9 la plus \u00e9l\u00e9gante mais lors d'un POC que je tentais de r\u00e9aliser je me suis rendu compte que cela serait un peu trop compliqu\u00e9 et long \u00e0 faire. Sans compter le fait que les rediffusions de Grand Prix ne sont pas g\u00e8r\u00e9es de la m\u00eame mani\u00e8re que les diffusions en live. Et que pour faire des Tests en live il faudrait attendre \u00e0 chaque fois un weekend de Grand Prix et le faire en plus du commentaire que je dois produire. Pour toutes ces raisons et bien d'autres je l'ai rang\u00e9e dans la case \"Trop dur, Trop chiant, S\u00fbrement ill\u00e9gal\" (Oui je sais c'est une cat\u00e9gorie bien sp\u00e9cifique mais c'est ma documentation je fais ce que je veux) La troisi\u00e8me option aurait \u00e9t\u00e9 la plus simple (et moins dr\u00f4le) et je suis presque s\u00fbr que je peux impl\u00e9menter cette derni\u00e8re en moins d'une apr\u00e8s-midi. Sauf qu'elle apporte de gros soucis. On ne peux pas garantir l'int\u00e9grit\u00e9 et la continuit\u00e9 des donn\u00e9es si l'utilisateur avance ou fait pause m\u00eame par simple inadvertance. La moindre fen\u00eatre qui s'afficherait devant ruinerait toute la reconnaissance de caract\u00e8res. On ne peut pas contr\u00f4ler la qualit\u00e9 du flux et on est oblig\u00e9 de faire confiance en l'utilisateur On ne peut pas vraiment automatiser quoi que ce soit niveau tests ou m\u00eame pour faire du scrapping auto pour remplir une base de donn\u00e9e. Et finalement le pire inconv\u00e9nient : C'EST NUL ! Je ne pourrais jamais utiliser un projet qui fonctionne de cette facon, je ne peux pas me permettre d'avoir un \u00e9cran inutilisable quand je commente et auquel je dois constamment faire attention pour ne pas perturber la reconnaissance. Pour moi cette option aurait \u00e9t\u00e9 celle \u00e0 choisir en cas d'extr\u00eame urgence et en dernier recours car le projet deviendrait inutile. J'ai donc d\u00e9cid\u00e9 de m'occuper de la seconde option : Simuler un navigateur. Cette option bien que complexe et difficile \u00e0 impl\u00e9menter propose une solution \u00e0 tous les probl\u00eame et permet une r\u00e9cup\u00e8ration quasi sans compromis.","title":"Comment faire ?"},{"location":"index.html#simuler-un-navigateur","text":"Simuler un navigateur internet n'est pas forc\u00e9ment tr\u00e8s difficile. Chromium par exemple offre une panoplie d'outils natifs et \u00e9norm\u00e9ment de librairies existent permettant de facilement et en quelques lignes simuler un Google Chrome et le contr\u00f4ler sans afficher son UI. \"Chromium logo\" {: style=\"height:150px;width:150px\"} Cependant. La F1TV n'utilise pas simplement un player HTML5 basique. Elle utilise un service de streaming BitMovin qui permet de fournir un stream de bonne qualit\u00e9 et surtout qui impl\u00e9mente les DRM (Digital Right Management) Cela veut dire que quand on ouvre un flux de la F1TV sur chrome et que l'on essaie de prendre une capture d'\u00e9cran, le player se met en noir et ne permet pas de voir quoi que ce soit (Certaines version de Chrome le permettent pendant quelques semaines avant de bloquer \u00e0 nouveau). Ce qui dans notre cas est un immense probl\u00e8me. Mais Firefox ne nous bloque pas de cette facon et il est donc assez facile de passer outre. L'explication sans trop rentrer dans les d\u00e9tails est la suivante : Dans chrome, le player par d\u00e9faut utilise une technologie appell\u00e9e \"PCP\" ou \"Protected Content Playback\" qui leur permet de bloquer au moins une partie des techniques de r\u00e9cup\u00e8ration du flux vid\u00e9o et audio. Cependant Firefox de pas sa nature Open Source utilise \"OpenH264\" pour lire ces m\u00eames flux soumis \u00e0 des DRM et OpenH264 n'impl\u00e9mente pas les m\u00eames restrictions. Sauf que Firefox n'est pas aussi facilement \u00e9mul\u00e9 que chrome et cela r\u00e9duit notre choix de librairies \u00e0 ... Une seule... Qui est Selenium. (Il existe aussi Pupetteer C# mais j'ai rencontr\u00e9 \u00e9norm\u00e9ment de soucis avec cette derni\u00e8re d\u00e8s que je voulais lancer une vid\u00e9o) \"Firefox dev logo\" {: style=\"height:150px;width:150px\"} Mais m\u00eame si la documentation est plut\u00f4t maigre parfois, c'est une bonne librairie qui permet de tr\u00e8s bien contr\u00f4ler une instance de chrome ou de Firefox.","title":"Simuler un navigateur ?"},{"location":"index.html#controler-le-navigateur","text":"Maintenant que l'on sait quel navigateur simuler et avec quelle technologie, on peut passer \u00e0 la r\u00e9alisation. Ce qu'il y a de bien avec Selenium, c'est qu'on a un certain nombre de commandes tr\u00e8s haut niveau qui nous permettent de contr\u00f4ler un navigateur de mani\u00e8re plut\u00f4t pr\u00e9cise. Je vais d\u00e9crire ici la proc\u00e9dure habituelle utilis\u00e9e sous une forme de recette de cuisine pour que l'on puisse facilement comprendre ce qu'il se passe. Durant cette explication je vais parler \u00e0 un moment de Cookies, ne vous en faites pas c'est le sous chapitre suivant qui va vous en parler. Recette de cuisine pour r\u00e9cup\u00e8rer des images de la F1TV : D\u00e9marrer une instance de navigateur avec les bons arguments Ajouter les bons param\u00eatres pour ne pas se faire flag comme un bot Naviguer sur la page de la F1TV Ajouter les cookies de connexion pour avoir acc\u00e8s au contenu de la page Naviguer sur la page du Grand Prix demand\u00e9 Attendre un peu que la page se charge Cliquer sur l'invite de cookies Attendre cinq secondes le temps que la page se reload Cliquer sur le bouton qui permet de passer du feed live \u00e0 la DATA CHANNEL Appuyer sur Espace pour faire apparaitre le bouton d'acc\u00e8s au param\u00eatres Cliquer sur le menu d\u00e9roulant des r\u00e9solution Trouver l'option 1080P et la selectionner Cliquer sur le bouton qui met la vid\u00e9o en plein \u00e9cran Prendre de screenshots \u00e0 intervales r\u00e9guliers Pour faire toutes ces actions on doit r\u00e9cup\u00e8rer les \u00e9l\u00e9ments selon leur ID ou leur classe. Voici un exemple qui r\u00e9cup\u00e8re le bouton de plein \u00e9cran et qui clique dessus : IWebElement fullScreenButton = Driver.FindElement(By.ClassName(\"bmpui-ui-fullscreentogglebutton\")); fullScreenButton.Click(); Ca peut para\u00eetre plut\u00f4t simple dit comme ca et quand tout fonctionne ca l'est mais la difficult\u00e9 vient du fait que \u00e0 peu pr\u00e8s nimporte laquelle de ces \u00e9tapes peut rater et qu'il faut donc faire un bon syst\u00e8me de gestion d'erreurs qui puisse aider l'utilisateur en cas de probl\u00e8me. Il faut dire aussi que les sites ne sont pas forc\u00e9ment tr\u00e8s content de voir des bots passer car cela peut \u00eatre un risque de DDOS et de Scraping (Comme moi) et donc ils mettent en place des syst\u00e8mes pour nous emp\u00eacher de faire ce que l'on veut On peut utiliser diff\u00e9rntes techniques pour passer outre ces restrictions comme : Changer son UserAgent Changer sa r\u00e9solution Ne pas avoir des patterns trop pr\u00e9visibles Avoir un historique Ne pas cliquer pile sur le milieu des boutons Ne pas cliquer trop vite Passer par un proxy pour ne pas se faire flag Utiliser des librairies plus discr\u00e8tes J'ai eu l'occasion de tester toutes ces methodes pour tenter de passer derri\u00e8re les radars de la F1TV et visiblement j'ai r\u00e9ussi pour les pages principales mais pas pour les pages de Login. Il faut savoir que la bataille entre bots et propri\u00e9taires de sites est un grand jeu du chat et de la souris et que les plateformes innovent constamment leur s\u00e9curit\u00e9. Et il se trouve que la partie login de la F1TV est heberg\u00e9e autre part que le reste du site chez Amazon et que elle poss\u00e8de les meilleures s\u00e9curit\u00e9s que j'aie pu voir. Aucunes des methodes que j'ai cit\u00e9es et d'autres encore que j'ai essay\u00e9 n'ont r\u00e9ussi \u00e0 fourvoyer le syst\u00e8me. J'ai donc \u00e9t\u00e9 oblig\u00e9 de faire appel \u00e0 la connexion par Cookies pour pouvoir acc\u00e8der au reste du site internet.","title":"Contr\u00f4ler le navigateur"},{"location":"index.html#recuperer-les-cookies","text":"Alors, on va mettre de c\u00f4t\u00e9 toutes les questions de s\u00e9curit\u00e9 et de violation de la vie priv\u00e9e et de protection des donn\u00e9es des utilisateurs pour ce chapitre. Car pour faire simple, je siphonne TOUS les cookies de la persone qui utilise mon app. [FINIR CETTE EXPLICATIOn]","title":"R\u00e9cup\u00e8rer les cookies ?"},{"location":"index.html#calibration","text":"[AJOUTER EXPLICATION]","title":"Calibration"},{"location":"index.html#ocr","text":"Ici je vais parler de la seconde partie du projet qui parle du processus de reconnaissance de data sur une image du feed DATA de la F1TV. C'est je pense la partie qui a demand\u00e9 le plus tests et de refactor. Toute la partie OCR a \u00e9t\u00e9 d\u00e9velopp\u00e9e dans un projet \u00e0 part avant d'\u00eatre int\u00e9gr\u00e9e dans le projet final. Il faut savoir que la reconnaissance est diff\u00e9rente celon ce que l'on cherche. Je vais donc d\u00e9composer cette partie du document en sous rubriques selon les donn\u00e9es recherch\u00e9es. Mais avant ca je dois expliquer certains concepts qui seront importants.","title":"OCR"},{"location":"index.html#fonctionnement-general","text":"Voici un screenshot de la page DATA de la F1TV que le programme va recevoir : \"Screen F1TV\" Si on regarde de loin on peut se dire que la structure est plut\u00f4t simple mais c'est loin d'\u00eatre le cas. On peut y voir au moins 4 zones contenant de l'information dans un format diff\u00e9rent. \"Zones principales\" Dans l'exemple ci dessus on peut voir 3 zones mais on aurait \u00e9galement pu comprendre la zone de position des pilotes autour du circuit pour faire 4. Ces 4 zones sont tr\u00e8s diff\u00e9rentes et contiennent d'autres informations. Pour ce travail de dipl\u00f4me je ne m'occupe que de la zone principale. Mais je pense que le titre et les infos de circuit ne prendrait pas tant de temps que ca \u00e0 impl\u00e9menter. J'ai utilis\u00e9 le mot \"Zone\" plus haut et ca n'est pas juste un mot utilis\u00e9 au hasard. C'est le nom de l'objet que j'utilise pour les repr\u00e9senter dans mon programme. Mais comme c'est important de bien comprendre ce concept avant de continuer je vais vous l'expliquer. ZONE : L'objet \"Zone\" parent est un objet qui est une zone d'image. Je m'explique, le but d'une zone est d'\u00eatre un morceau d'une image plus grande. Le but d'une Zone est de contenir une liste de plus petites Zones ou bien une liste de \"Window\" (j'explique ce que c'est juste apr\u00e8s). Elle contient la portion d'image qui la concerne et ses propres dimensions. Le parent zone ne pr\u00e9voit que de pouvoir ajouter ou supprimer des \u00e9l\u00e9ments des listes de zones ou de windows ainsi qu'une methode qui permet d'aller chercher toutes informations des livres qu'elle contient. L'int\u00e9r\u00eat d'une zone est de pouvoir compartimenter une image dans des parties int\u00e9ressantes au niveau de la reconnaissance mais pas de traiter d'information. WINDOW : L'objet \"Window\" est un objet qui peut ressembler beaucoup \u00e0 l'objet \"Zone\". En effet elle aussi est une partie d'une image plus grande et contient ses dimensions, mais elle se distingue en deux points importants. Elle ne contient pas d'autres Zones ou Windows Elle peut retourner les informations \u00e9crites sur son image. Toutes les Window qui h\u00e9ritent du parent Window peuvent impl\u00e9menter une methode qui permet de renvoyer ce qui peut \u00eatre d\u00e9cod\u00e9 sur son image. Les enfants peuvent aussi aller piocher dans les nombresues methodes de r\u00e9cup\u00e8ration de donn\u00e9es contenues dans le parent Window. Mieux vaut r\u00e9utiliser le plus possible que de r\u00e9inventer la roue pour chaque Window. Une analogie un peu bancale pourrait se pr\u00e9senter comme la suivante : La zone est une armoire ou une bibliot\u00e8que. Si c'est une zone qui contient d'autres zones c'est une bibliot\u00e8que et chacune de ces sous-zones sont des armoires. Leur unique but est de contenir de mani\u00e8re ordonn\u00e9e des objets qui eux contiennent de l'information. Les livres ici sont les Windows. Ils contiennet de l'information et sont stock\u00e9s dans des armoires et on y acc\u00e8de en allant dans la bonne bibliot\u00e8que et en allant dans la bonne armoire. Derni\u00e8res choses pour comprendre le diagramme: Il existe une Main Zone qui est une des 4 grandes zones dont je parlais dans la d\u00e9composition de l'image. Il existe aussi des \"Driver Zone\" qui sont de plus petites zones contenues dans la Main Zone qui et qui ne contiennent que les informations d'un pilote. L'objet Window n'est quasi jamais utilis\u00e9, c'est presque tout le temps des enfants de Window plus sp\u00e9cifiques qui sont utilis\u00e9s, le but est que chaque type d'information sur l'image aie son type de window. Voila donc un petit diagramme qui montre le d\u00e9coupage du programme : \"Diagramme explicatif de l'architecture des zones\" Pour visualiser encore un peu mieux comment ce d\u00e9coupage prend forme voici ce que chaque zone et Window contient. Main Zone : \"Exemple zone principale\" Driver Zone : \"Exemple zone de pilote\" Driver Position Window : \"Exemple de fen\u00eatre de position\" Driver name Window : \"Exemple de fen\u00eatre de nom\" Driver LapTime Window : \"Exemple de fen\u00eatre de temps au tour\" Driver Tyre Window : \"Exemple de fen\u00eatre pneus\" Il existe d'autres types de Window mais ce sont les principaux. On se rend assez facilement compte que chacunes de ces windows va avoir besoin d'un traitement sp\u00e9cifique car la mani\u00e8re de reconnaitre le pneu utilis\u00e9 et le temps au tour ne peut pas \u00eatre la m\u00eame. Pour r\u00e9sumer, on a un programme qui prend en entr\u00e9e un fichier de configuration, qui prend des images de la F1TV et les d\u00e9coupe dans des ZONES qui elles m\u00eame sont d\u00e9coup\u00e9es en WINDOWS pour qu'on puisse plus facilement les d\u00e9coder. Maintenant qu'on a une liste de diff\u00e9rent types de zones on peut commencer \u00e0 chercher ce qu'il y a marqu\u00e9 dessus. Pour cela il faut dabord comprendre un petit peu comment l'OCR fonctionne et comment des libraries comme Tesseract fonctionnent pour donner du texte en partant d'une image. Pour faire tr\u00e8s simple, nous avons un mod\u00e8le qui est entrain\u00e9. C'est \u00e0 dire que on donne \u00e0 un programme un tr\u00e8s grand nombre de mots ou de lettres en lui disant ce que contiennent chaques images. Ensuite le programme va cr\u00e9er des matrices de convolutions pour chaque lettre avec comme objectif de detecter les points communs entre les lettres pour cr\u00e9er un alpphabet. Par exemple la matric de la lettre 'H' donnerait un poids important \u00e0 des lignes verticales connect\u00e9es par une ligne centrale. Et si on fournis assez de donn\u00e9es de bonne qualit\u00e9 au mod\u00e8le, les matrices peuvent \u00eatre tr\u00e8s efficace \u00e0 detecter si une lettre est un H ou un M. Il y a pleins d'autres methodes comme l'utilisation d'un dictionnaire de mots de la langue pour permettre la reconnaissance de mots m\u00eame si une lettre au milieu n'est pas comprise ou en ajoutant d'autres informations sur le contexte mais ca ne nous int\u00e9resse pas ici. C'est important de comprendre comment cette reconnaissance de caract\u00e8res avec des matrices fonctionne car cela va nous aider \u00e0 pr\u00e9parer nos donn\u00e9es pour lui rendre la vie facile et augmenter la pr\u00e9cision de nos r\u00e9sultats.","title":"Fonctionnement g\u00e9n\u00e9ral"},{"location":"index.html#filtres-et-traitement","text":"On peut essayer de donner toutes nos images directement \u00e0 Tesseract pour qu'il reconnaisse tout le texte qu'il y voit mais on risque de se retrouver avec des r\u00e9sultats au mieux inconsistents. Dans notre cas, le soucis est que les chiffres et lettres sont beaucoup trop petits. Ils ne font parfoisd que 10 pixels de haut et cela fait que il n'est pas forc\u00e9ment facile de toujours les diff\u00e9rencier. De plus, comme ils sont petits, les art\u00e9facts d'aliasing sont assez violents et peuvent grandement d\u00e9former une lettre ou un chiffre. Exemple : Prenons le chiffre 9. Dans l'image il peut \u00eatre repr\u00e9sent\u00e9 de cette mani\u00e8re : \"Exemple de chiffre avant post traitement\" On peut voir qu'il est flou, pour nous cela ne pose pas de probl\u00e8me et je pense que \u00e0 peu pr\u00e8s nimporte qui peut dire que c'est un 9. Cependant comme les contours sont flous et m\u00eame si on essaie de retirer le background : \"9 avec anti aliasing\" On voit que le 9 n'est pas clairement d\u00e9finit. En effet on pourrait le comprendre comme : \"Premier exemple de contours\" Ou comme : \"Second exemple de contours\" Voire m\u00eame simplement comme : \"Exemple de coutour g\u00e9n\u00e9reux\" Et on se rend bien compte que les performances de detection ne sont pas les m\u00eames dans ces trois cas. Il faut donc faire un certain post traitement des images pour supprimer les \u00e9l\u00e9ments parasites, les couleurs, et augmenter la visibilit\u00e9 des contours importants. Mais chaque type de donn\u00e9e va avoir des methodes de post traitement diff\u00e9rents. Donc voici les diff\u00e9rents types de reconnaissance et leur post traitements :","title":"Filtres et traitement"},{"location":"index.html#texte","text":"Alors ce type de reconnaissance est utilis\u00e9 par la WINDOW du nom de pilote et de la position du pilote. C'est je pense la plus simple de toutes car Tesseract est particuli\u00e8rement bien entrain\u00e9 pour. Cette reconnaissance concerne donc des lettres qui font des mots ou des noms. Voici un exemple de la WINDOW nom de pilote en entr\u00e9e : \"Exemple texte cru\" Ce texte peut paraitre bon, cependant quand on le lance dans Tesseract, il ne va pas toujours donner un r\u00e9sultat parfait. Il faut aussi savoir qu'il y a des noms pas mal plus p\u00e9nibles que Tesseract a plus de mal \u00e0 reconnaitres, soit \u00e0 cause des lettres utilis\u00e9es, soit car le nom est un nom d'une autre r\u00e9gion et qui ne veut rien dire en anglais ce qui emp\u00eache l'utilisation de dictionnaire (Ex : Tsunoda est un nom japonais et parfois il est difficile pour Tesseract de le reconnaitre car si une lettre pose probl\u00eame il ne peut pas trouver de contexte qui puisse l'aider). Donc pour le rendre plus facilement lisible et augmenter les chances que toutes les lettres soient d\u00e9couvertes, voici les \u00e9tapes que j'ai mis en place. 1 : J'inverse les couleurs. Je me suis rendu compte que il \u00e9tait souvent plus facile de trouver un noir sur blanc que blanc sur noir. Je ne suis pas sur que cette \u00e9tape soit capitale cependant. \"Texte invers\u00e9\" 2 : Je fais un Treshhold de 165 car avec moins le texte parfois prend trop du background et avec plus les lettres sont trop fines. \"Texte apr\u00e8s Treshold\" 3 : Je fais un Resize de l'image pour avoir une meilleure r\u00e9solution et permettre une meilleure d\u00e9tection. J'augmente la hauteur et la largeur par un facteur 2. J'ai trouv\u00e9 cette valeur suffisante et aller plus haut consomme beaucoup de ressources. \"Texte apr\u00e8s Resize\" 4: Je fais une tr\u00e8s rapide Dilatation du texte pour retirer le flou amen\u00e9 par la methode de Resize. Je n'utilise qu'une valeur de 1 car je ne veux pas trop changer comment le texte est model\u00e9 je veux juste retirer le flou. \"Texte apr\u00e8s Dilatation\" Explication des methodes pr\u00e9cises plus bas Voila pour ce qui est du post processing. Je ne dis pas que ce sont les meilleurs param\u00eatres possibles mais dans mes tests ce sont ceux qui ont le mieux march\u00e9s. C'est aussi les premi\u00e8res methodes que j'ai pu d\u00e9velopper alors forc\u00e9ment elles n'ont pas le niveau de d\u00e9tails de certaines autres. Mais comme m\u00eame avec ce traitement il n'est pas rare que je me retrouve avec une ou deux lettres pas justes, il faut un moyen d'\u00eatre s\u00fbr que c'est le bon nom qui est trouv\u00e9. Ce qu'il y a de pratique avec les noms de pilotes c'est que on sait d\u00e9ja comment ils s'appellent avant le Grand Prix. En effet dans le fichier de configuration de la reconnaissance, il y a une liste de noms de pilotes. Cela veut dire que au lieu de chercher \u00e0 trouver parfaitement les bonnes lettres, on peut simplement essayer de trouver quel nom de pilote ressemble le plus au nom trouv\u00e9 sur l'image. Pour ce faire j'ai utilis\u00e9 une methode appel\u00e9e la distance de Levenshtein. Pour faire simple c'est une methode qui va calculer les distances de lettres pour determiner entre des strings laquelle ressemble le plus \u00e0 une autre. Pour r\u00e9sumer le fonctionnement dans lordre : On prend l'image on la traite On envoie l'image trait\u00e9e \u00e0 Tesseract On trouve quel nom de pilote ressemble le plus \u00e0 ce r\u00e9sultat On renvoie le nom du pilote","title":"Texte"},{"location":"index.html#chiffres","text":"Cette methode en r\u00e9alit\u00e9 utilise simplement la m\u00eame methode que celle qui va r\u00e9cup\u00e8rer le texte sur une image. Cependant, la, on envoie \u00e0 Tesseract l'information qu'il ne peut trouver que des chiffres sur l'image ce qui lui permet d'\u00eatre beaucoup plus pr\u00e9cis et de ne pas confondre un 9 avec un P ou un 11 avec un H PAR EXEMPLE (non pas que ca me soit arriv\u00e9 tr\u00e8s r\u00e9guli\u00e8rement et que ca me soit rest\u00e9 dans la gorge \u00e9videmment) L'avantage c'est que cette methode ne demande m\u00eame pas de traitement de la donn\u00e9e en sortie de Tesseract. On \u00e9sp\u00e8re simplement que le post traitement aura suffit. TEMPS : Cette methode regroupe la d\u00e9tection de temps au tour. Il y a trois grands types de WINDOW qui sont concern\u00e9es : La WINDOW du temps au tour La WINDOW du retard sur le leader La WINDOW des secteurs La grande diff\u00e9rence ce sont les ordres de grandeur. Les temps au tour sont en g\u00e9n\u00e9ral entre 50 secondes et 2 minutes. Tandis que les secteurs sont entre 20 et 30 secondes alors que le retard sur le leader peut-\u00eatre de plusieurs minutes. Cependant, tous ces temps poss\u00e8dent le m\u00eame type de post-traitement avant d'\u00eatre envoy\u00e9s \u00e0 Tesseract. Voici un exemple de temps au tour avant toute transformation : \"Temps au tour avant traitement\" On peut avoir l'impression que ce texte est tout \u00e0 fait lisible et facile \u00e0 d\u00e9coder surtout quand on le voit de loin comme ca. Cependant, il faut imaginer que ces chiffres font 13 pixels de haut en comptant le flou et comme expliqu\u00e9 plus haut ce flou dans ces echelles est terrible. \"Temps au tour zoom\u00e9\" Si on donne cette image \u00e0 Tesseract, les '3' deviennent des '9', des '9' deviennent des '8', des '2' deviennent eux aussi des '9', le tout parfois inversement et de mani\u00e8re compl\u00eatement impr\u00e9visible. Ca n'est simplement pas utilisable. Cette partie est un peu plus complexe car si la detection n'est pas fiable les chiffres sont simplement inutilisables. Si \u00e0 tout moment un temps au tour de 1:39.106 devient 1:32.108 c'est juste pas possible. Voici donc les \u00e9tapes de post-traitement que j'ai mis en place pour leur d\u00e9tection : 1: J'applique un Treshold de 185 pour enlever les ambiguit\u00e9s d'alisaising et avoir une image en noir et blanc claire. La valeur de 185 est assez \u00e9lev\u00e9e car le but est de vraiment garder uniquement les contours. Comme les chiffres se ressemlent beaucoup plu que les lettres, il faut tenter le plus possible de conserver leur formes sp\u00e9cifiques. Je me suis rendu compte que cette valeur \u00e9tait une de celles qui marchent le mieux. \"Temps au tour apr\u00e8s Treshold\" 2: J'applique un Resize de 2 pour augmenter la r\u00e9solution des chiffres et permettre une meilleure d\u00e9tection. Le but est d'avoir plus de pixels et donc de permettre \u00e0 Tesseract de mieux utiliser ses matrices de convolution. \"Temps au tour apr\u00e8s Resize\" 3: Comme le Resize am\u00e8ne du flou, j'utilise une methode de Dilatation qui me permet de retirer ce flou et de remplir un peu plus certaines parties qui ont \u00e9t\u00e9 un peu laiss\u00e9e par le Resize ; \"Temps au tour apr\u00e8s Dilatation\" 4: Contrairement aux mots plus haut, la rondeur ajout\u00e9e par la dilatation n'est pas vraiment d\u00e9sir\u00e9e. En effet, elle peut rendre confuse certains chiffres et emp\u00eacher Tesseract de bien trouver le chiffre. Alors j'applique une Erosion qui me permet de contrecarrer en partie les rondeurs ajout\u00e9es par la dilatation et retrouver des chiffres bien form\u00e9es. Pour l' Erosion et la Dilatation j'ai utilis\u00e9 une valeur de 1 car je ne voulais pas trop changer les chiffres. \"Temps au tour apr\u00e8s Erosion\" Explication des methodes pr\u00e9cises plus bas Et avec ce post processing on retrouve de plut\u00f4ts bon r\u00e9sultats qui demandent peu de traitement. Le traitement d\u00e9pend du type de WINDOW cependant. Pour les secteurs on indique \u00e0 Tesseract que les caract\u00e8res autoris\u00e9s sont : \"0123456789.\" Pour les temps au tour on autorise plut\u00f4t \"0123456789.:\" Et pour les \u00e9carts on autorise \"0123456789.+\" Ensuite on r\u00e9cup\u00e8re une liste de chiffres qui'il va falloir transformer en milisecondes pour faciliter le stockage et l'envoi. Le programme nettoie un peu la chaine avant de la convertir. Par exemple parfois le ':' de 1:34.456 est compris comme un '1' ou un '2' et il faut faire attention \u00e0 detecter quand ca arriver. Je passe les d\u00e9tails du reste du nettoyage car c'est vraiment du cas par cas mais quand on a finit de nettoyer la chaine on peut transformer les chaines de minutes secondes et milisecondes en un total de milisecondes. Pour r\u00e9sumer le fonctionnement dans l'ordre : On prend l'image et on lui applique une s\u00e9rie de filtres On envoie l'image filtr\u00e9e \u00e0 Tesseract On nettoie le r\u00e9sultat Tesseract pour compenser certains biais On convertis le r\u00e9sultat en milisecondes","title":"Chiffres"},{"location":"index.html#pneus","text":"La on arrive sur la partie la plus p\u00e9nible. Pour comprendre la probl\u00e9matique il faut d'abord faire un petit point sur comment les pneus fonctionnent en Formule 1. Depuis 2019 en Formule 1 nous avons 5 grandes familles de pneus : Les pneus tendres Les pneus medium Les pneus durs Les pneus interm\u00e9diaires Les pneus pluie \"Gamme de pneus Pirelli\" Les trois premiers pneus sont des pneus faits pour piste s\u00e8che, le pneu interm\u00e9diaire pour piste humide et le neu pluie pour la pluie. Chaque pneu a sa dur\u00e9e de vie et son niveau de performance propre mais je ne vais pas rentrer dans le d\u00e9tail ici. Tout ce qu'il faut savoir ce que savoir sur quel pneu chaque pilote est et depuis combien de temps il les chausse est une information tr\u00e8s importante. Chaque pneu a une couleur donn\u00e9e qui permet de les diff\u00e9rencier. Voici un exemple de ce \u00e0 quoi une WINDOW de pneus peut ressembler : \"Exemple zone pneus 1\" Mais cette zone peut aussi ressembler \u00e0 ca : \"Exemple zone pneus 2\" Mais aussi \u00e0 ca : \"Exemple zone pneus 3\" Voire m\u00eame ca : \"Exemple zone pneus 4\" Je pense que vous pouvez tout de suite comprendre la difficult\u00e9 que repr\u00e9sente la t\u00e2che de r\u00e9cup\u00e8ration de donn\u00e9es \u00e0 partir de cette image. En gros le fonctionnement de cette zone d'information est assez simple. Au fur et \u00e0 mesure que la course avance, le trait fait de m\u00eame. Le chiffre dans le round tout \u00e0 droite indique le nombre de tour que le pilote a pass\u00e9 sur ce pneu. La couleur indique le type de pneu. Si il y a une lettre \u00e0 la place d'un chiffre c'est que c'est le premier tour sur ce pneu. La lettre indique le type de pneu. Et pas besoin de dire que si on essaie simplement de donner l'image \u00e0 Tesseract on ne r\u00e9cup\u00e8re ni les chiffres ni les lettres correctement si ce n'est pas du tout. Il faut donc utiliser une methode qui permette d'isoler le rond le plus \u00e0 droite, lui appliquer un traitement qui permette \u00e0 Tesseract de lire ce qu'il y a marqu\u00e9 et qui puisse determiner quel pneu est en train d'\u00eatre utilis\u00e9. J'ai d\u00e9cid\u00e9 de m'occuper dans un premier temps de trouver ce rond avant d'appliquer les filtres car plus l'image est petite plus les filtres sont rapides. Le programme va tirer un trait depuis le bord droit de la zone, et il va avancer vers la gauche jusqu'\u00e0 trouver un obstacle. Je d\u00e9tecte un obstacle si le pixel sur lequel est mon trait poss\u00e8de une valeur de plus de 0x50 dans le channel R,G ou B. J'ai trouv\u00e9 en faisant des tests que les couleurs de background de la F1TV ne d\u00e9passaient jamais ces valeurs. Ensuite apr\u00e8s avoir trouv\u00e9 le premier obstacle, je r\u00e9cup\u00e8re une zone qui doit englober le cercle. Voici un exemple avec cette image en entr\u00e9e : \"Zone compl\u00eate\" Elle est automatiquement coup\u00e9e de cette facon : \"Zone coup\u00e9e automatiquement\" Cela me permet d'isoler uniquement ce qui m'int\u00e9resse ce qui est tr\u00e8s pratique pour Tesseract et pour la detection de couleur. Ensuite avec cette image je peux commencer le processus de reconnaissance. Je commence par faire une moyenne de tous les pixels de l'image en excluant les pixels trop sombres qui font s\u00fbrement partie du background ou du chiffre. Ensuite j'utilise une methode qui calcule la diff\u00e9rence entre la couleur obbtenue et la liste de couleurs possible. Il y a cinq couleurs des pneus possibles : \"#ff0000\" pneu tendre/soft \"Couleur d'un pneu tendre\" \"#f5bf00\" pneu medium \"Couleur d'un pneu medium\" \"#a4a5a8\" pneu dur/hard \"Couleur d'un pneu dur\" \"#00a42e\" pneu inter \"Couleur d'un pneu interm\u00e9diaire\" \"#2760a6\" pneu pluie/wet \"Couleur d'un pneu pluie\" Ce qui est pratique c'est que m\u00eame dans les cas ou il n'y a pas beaucoup de couleur comme celui la : \"Pneu dur avec 0 tours\" On arrive \u00e0 une couleur moyenne de : \"Couleur moyenne de l'image ci dessus apr\u00e8s soustraction du background\" Et il est donc assez facile de determiner le type de pneu en question. Attention, les r\u00e9sultats peuvent \u00eatre tr\u00e8s vite d\u00e9rang\u00e9s par la couleur du pneu pr\u00e9c\u00e9dent si le d\u00e9coupage de la fen\u00eatre n'a pas \u00e9t\u00e9 assez pr\u00e9cis. Ensuite il \"suffit\" de lire le chiffre dans le rond et si on arrive pas \u00e0 le lire alors c'est que c'est une lettre et on sait que le nombre de tours est donc de 0. Maintenant vient le moment tr\u00e8s sympatique de la lecture du chiffre. Vous saurez que Tesseract en plus de detester les grandes images et les images avec des couleurs, deteste \u00e9galement les formes dans une image. Donc dans notre cas, le round de couleur autour du chiffre, m\u00eame si il n'est pas complet, il interf\u00e8re avec la reconnaissance et emp\u00eache de bien lire le chiffre. Il faut donc retirer le background et ensuite la couleur. Sauf que comme le chiffre est de la couleur du background, si on retire le background et ensuite la couleur il ne reste plus rien. Il faut donc retirer le background AUTOUR du rond, et ensuite si on retire la couleur il devrait rester le chiffre sur fond blanc. Pour se faire, j'ai tir\u00e9 des traits depuis les bords de l'image jusqu'\u00e0 ce qu'ils rencontrent le rond. Ensuite je retire tous les pixels entre le rond et les bords de l'image ce qui nous donne ceci : \"Zone pneu avec le background en moins\" Ensuite on peu retirer les pixels qui ont une valeur dans un channel RGB plus haute qu'un certain seuil : \"Zone avec le reste des couleurs supprimm\u00e9es\" Et la on a ce que l'on veut ! A partir de la c'est les filtres que l'on connait qui sont utilis\u00e9s pour en faire une image plus facile \u00e0 utiliser par Tesseract. 1 : On effectue un Resize de facteur 4 (oui c'est beaucoup mais en m\u00eame temps le chiffre est vraiment petit \u00e0 la base) qui permet d'avoir une image d'une bien meilleure r\u00e9solution. \"Filtre 1\" 2: On fait une Dilatation de facteur 1 pour retirer tout le flou de l'image pour aider Tesseract \"Resultat\" Et on a un chiffre qui est utilisable par Tesseract ! Explication des methodes pr\u00e9cises plus bas Pour r\u00e9sumer : On prend l'image de la zone et on la crop pour ne garder que la partie essentielle On d\u00e9termine le type de pneu avec la couleur moyenne de la zone On retire le background autour de cette zone On retire la couleur qui reste pour ne garder que le chiffre On augmente la r\u00e9solution du chiffre On rend ce chiffre net On envoie l'image trait\u00e9e et filtr\u00e9e \u00e0 Tesseract On d\u00e9termine le nombre de tours que le pilote a fait avec ses pneus avec le r\u00e9sultat de Tesseract","title":"Pneus"},{"location":"index.html#drs","text":"Bon ca c'\u00e9tait plut\u00f4t simple j'ai simplement v\u00e9rifi\u00e9 si la moyenne de vert d\u00e9passait une certaine valeur et puis voila.","title":"DRS"},{"location":"index.html#filtres-et-methodes-sur-les-images","text":"Dans ce projet on a du utiliser diff\u00e9rentes methodes d'\u00e9dition d'image que ce soit sous forme de filtres ou de modification de l'image directement. Voici un sommaire des methodes utilis\u00e9es et comment elles fonctionnent. Tresholding Cette methode sert \u00e0 passer d'une image en couleurs \u00e0 une image binaire noir blanc. C'est une \u00e9tape tr\u00e8s importante pour l'OCR car elle permet (si bien faite) d'isoler du texte de son background. Un exemple ici : \"Exemple treshold\" Le fonctionnement est assez simple mais il peut \u00eatre fait de diff\u00e9rentes mani\u00e8res mais dans mon cas voici comment l'algorythme fonctionne sachant qu'il demande en entr\u00e9e la Bitmap que l'on veut modifier ainsi que la valeur de Treshold : On parcours chaque pixel de l'image On convertir la couleur du pixel en une valeur de gris pour avoir la m\u00eame valeur en R,G et B (Formule utilis\u00e9e : grey = R x 0.3 + G x 0.59 + B x 0.11) Si le r\u00e9sultat de la valeur de gris est au dessus de la valeur de treshold, le pixel est pass\u00e9 en blanc complet et dans le cas contraire il est pass\u00e9 en noir complet On retourne la Bitmap modifi\u00e9e Un algorythme pas forc\u00e9ment complexe mais qui peut augmenter de mani\u00e8re titanesque les chances de r\u00e9ussir une OCR Resize Cette methode sert \u00e0 augmenter la r\u00e9solution d'une image pour am\u00e9liorer la pr\u00e9cision de l'algorythme de Tesseract. En effet, avec trop peu de pixels, la matrice de convolution n'est pas toujours aussi efficace. Il ne faut pas confondre cette methode d'augmentation de la taille avec une simple interpolation. En effet une augmentation de taille interpol\u00e9e ne vas pas vraiment changer la r\u00e9solution, l'image sera toujours aussi pixelis\u00e9e, seulement, les pixels seront compos\u00e9es de plus de pixels comme dans l'exemple ci dessous : \"Exemple d'interpolation lin\u00e9aire\" Dans mon projet j'utilise de l'interpolation bicubique qui va cr\u00e9er de l'information pour tenter de combler le vide et produire une image r\u00e9ellement plus grande et avec plus de details mais en ajoutant du flou. \"Exemple des diff\u00e9rents types d'interpolation\" Le but est d'aller chercher dans les pixels alentours les couleurs qui sont d\u00e9ja pr\u00e9sente et de jouer avec des poids pour tenter de faire une pr\u00e9diction de ce que ce pixel aurait \u00e9t\u00e9 si l'image avait plus de detail. Voici un exemple assez parlant : \"Exemple interpolation bicubique (avant)\" \"Exemple interpolation bicubique (apr\u00e8s)\" On pourrait croire que c'est inutile mais dans le contexte de Tesseract ajouter des d\u00e9tails pour tenter de simuler une meilleure r\u00e9solution m\u00eame en cr\u00e9ant du flou est int\u00e9ressant pour mieux remplir la matrice de convolution. Mais il est possible de r\u00e9duire ce flou avec d'autres m\u00e9thodes \u00e9galement. (Dans mon code je n'ai pas utilis\u00e9 du code fait main mais j'utilise une librairie qui me permet de le faire) Il faut simplement faire attention car c'est un proc\u00e9d\u00e9 assez lourd en performances. Dilatation et Erosion Cette methode et la suivante font partie des methodes de transformation morphologiques. Ces methodes sont utilis\u00e9es pour accentuer les formes et les epaissir ou les r\u00e9duire et les affiner. Elles poss\u00e8dent l'aventage \u00e9galement de retirer le flou d'une image ce qui est tr\u00e8s pratique si utilis\u00e9 apr\u00e8s l'utilisation de methodes comme Resize . Je ne vais pas trop rentrer dans les d\u00e9tails de ces methodes car leur fonctionnement est un peu plus lourd en math si on veut faire une v\u00e9ritable explication du pourquoi et du comment ca marche aussi bien. Pour notre projet je dirais que l'important est de savoir que ce sont deux outils tr\u00e8s pratiques pour changer la morphologie des lettres et des chiffres et qu'on peut les utiliser pour corriger du flou et/ou des art\u00e9facts apparus lors de la binarisation de l'image ou de la suppression de fond. Remove Background Cette methode est assez simple et est juste une methode qui va passer en revue tous les pixels de l'image et si la couleur d'un pixel s'apparente \u00e0 celle d'un pixel de fond il est pass\u00e9 en noir total ou en blanc total. Le but est de permettre au reste du programme de fonctionner avec des couleurs moins ambigues. Une variante sp\u00e9cialis\u00e9e pour la reconnaissance des pneus appel\u00e9e affectueusement Remove Useless cherche \u00e0 atteindre le m\u00eame bu mais est bien plus soffistiqu\u00e9e et sp\u00e9cialis\u00e9e pour retirer le background autour d'un cercle de couleur pour ensuite retirer la couleur et qu'il ne reste qu'un chiffre. Pour plus de details voir la detection de pneus. Il y aussi d'autre methodes comme un filtre Gaussien ou Highlight countour que j'ai du d\u00e9velopper mais que je n'ai pas utilis\u00e9 donc je ne vais pas en parler ici.","title":"Filtres et methodes sur les images"},{"location":"index.html#interpretation-des-donnees","text":"","title":"Interpr\u00e9tation des donn\u00e9es"},{"location":"index.html#stockage-des-donnees","text":"","title":"Stockage des donn\u00e9es"},{"location":"index.html#affichage-des-donnees","text":"","title":"Affichage des donn\u00e9es"},{"location":"index.html#predictions","text":"","title":"Pr\u00e9dictions"},{"location":"index.html#tests","text":"[A remplir au fur et \u00e0 mesure de la cr\u00e9ation des tests]","title":"Tests"},{"location":"index.html#resume-des-difficultes-techniques","text":"[A remplir au fur et \u00e0 mesure dans la seconde moiti\u00e9 du travail de dipl\u00f4me]","title":"R\u00e9sum\u00e9 des difficult\u00e9s techniques"},{"location":"index.html#optimisation-du-programme","text":"[A remplir \u00e0 la fin du projet pour parler des diff\u00e9rentes methodes d'optimisation]","title":"Optimisation du programme"},{"location":"index.html#ethique-du-projet","text":"[A remplir \u00e0 la fin du projet pour parler des questions ethiques du projet (Ex: Utilisation potentiellement abusive de la F1Tv ou L'histoire des cookies)]","title":"Ethique du projet"},{"location":"index.html#ameliorations-futures","text":"[A remplir dans les derni\u00e8res semaines du travail de dipl\u00f4me]","title":"Am\u00e9liorations futures"},{"location":"index.html#conclusion","text":"[A remplir la derni\u00e8re semaine du travail de dipl\u00f4me]","title":"Conclusion"},{"location":"CahierDesCharges.html","text":"Cahier des charges Cahier des charges \"Track Trends\" Travail de dipl\u00f4me Maxime Rohmer 2023 Contexte Je suis le \"Live Ticker\" charg\u00e9 de la Formule 1 pour le 20 minutes. On peut traduire cela comme commentateur de F1, avec tout de m\u00eame l'importante subtilit\u00e9 que je ne commente pas avec la voix, mais avec le clavier. Mes commentaires sont sous la forme de commentaires \u00e9crits live qui s'ajoutent au fur et \u00e0 mesure de l'\u00e9v\u00e8nement. Par exemple : \"Tour 28/54, Hamilton a fini par s'arr\u00eater et chausser des gommes tendres 13 tours apr\u00e8s Verstappen. L'Anglais va voir plus de 15 secondes \u00e0 rattraper, mais les gommes neuves et plus tendres que son rival devraient lui permettre s'il ne se fait pas trop ralentir par le trafic\". En g\u00e9n\u00e9ral avec un peu plus d'infos quand m\u00eame et cela tous les 3-4 tours Voici quelques exemples de pr\u00e9c\u00e9dents commentaires (Conseil : il y a un bouton pour montrer le feed dans l'ordre chronologique) : \"Commentaire Grand Prix de Belgique 2022\" \"Commentaire du Grand Prix de Singapour 2022\" \"Exemple commentaires\" Pendant un Grand Prix, je dois constamment : \u00c9crire ce qu'il se passe dans le grand prix et expliquer les enjeux Chercher r\u00e9guli\u00e8rement des m\u00e9dias \u00e0 inclure pour diversifier mon live (Tweets, Images etc.) Changer le titre et la description du live en fonction de l'\u00e9volution du Grand prix Et accessoirement regarder le grand prix pour y comprendre quelque chose Avec tout \u00e7a, il est tr\u00e8s difficile de garder un \u0153il sur la page DATA de la F1TV qui fournit pourtant des informations pr\u00e9cieuses. Je me retrouve parfois par exemple \u00e0 ne pas parler de d\u00e9passements dans le peloton, car ils ne sont pas retransmis \u00e0 la t\u00e9l\u00e9 alors que c'est une information importante. Autre exemple, occasionnellement le classement ne refl\u00e8te pas les vraies positions des pilotes. Les arr\u00eats aux stands font que du coup des pilotes qui devraient \u00eatre 15\u00e8mes se retrouvent 8\u1d49 puisqu'ils ne sont pas encore arr\u00eat\u00e9s. Cela peut de temps en temps pr\u00eater \u00e0 confusion. Projet Un outil de style compagnon sous forme d'application C# Windows Form qui r\u00e9cup\u00e8re en temps r\u00e9el les informations de la course et affiche les informations les plus importantes. Le but est non seulement de faciliter mon job, mais aussi faire en sorte d'am\u00e9liorer la plus-value de mon travail en me permettant de fournir des commentaires qui ne sont pas disponibles pour le tout venant qui regarde simplement le flux RTS. Exemples: Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand Maintenant afficher diff\u00e9remment les infos, c'est sympa, mais cela serait encore mieux de traiter ces data et de permettre des petites pr\u00e9dictions. Exemples : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents R\u00e9alisation Malheureusement, la Formula 1 Management ne propose aucune API publique qui puisse nous permettre de faire ce projet \"simplement\". La raison la plus probable \u00e9tant qu'Amazon avec son service AWS propose exactement ce genre de services pour le flux t\u00e9l\u00e9vis\u00e9 et il doit y avoir un contrat d'exclusivit\u00e9. Il existe des API \"Pirates\" faites par la communaut\u00e9, le probl\u00e8me est qu'elles ne sont pas forc\u00e9ment des plus pratiques \u00e0 utiliser. Mais comme je poss\u00e8de un abonnement premium ++ \u00e0 la F1TV, j'ai acc\u00e8s pour chaque grand prix \u00e0 un flux vid\u00e9o nomm\u00e9 : DATA F1 CHANNEL Qui ressemble \u00e0 \u00e7a : \"Data channel exemple\" Donc la seule fa\u00e7on que je vois de r\u00e9cup\u00e9rer ces donn\u00e9es est de les prendre directement sur ce feed. M\u00eame si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout \u00eatre la r\u00e9cup\u00e9ration des donn\u00e9es et leur stockage. Les donn\u00e9es viennent du flux vid\u00e9o et ainsi dans un premier temps, il va falloir r\u00e9cup\u00e9rer d'une mani\u00e8re ou d'une autre des images qui viennent d'un grand prix en direct ou en rediffusion. Ensuite, dans un second temps, il faut lire les informations directement sur l'image en utilisant une librairie pr\u00e9vue pour (exemple Tesseract) et v\u00e9rifier l'int\u00e9grit\u00e9 de ces derni\u00e8res pour qu'on puisse ensuite les stocker. Dans un troisi\u00e8me temps, il faut stocker toutes ces donn\u00e9es dans une forme qui permette d'aller facilement faire des requ\u00eates de r\u00e9cup\u00e9ration et d\u00e9j\u00e0 pr\u00e9parer des m\u00e9thodes qui permettent de r\u00e9cup\u00e9rer des infos importantes (ex : la moyenne des cinq derniers tours, le temps moyen d'arr\u00eat etc.) pour faciliter la derni\u00e8re \u00e9tape Quand tout cela est fait, on peut ensuite s'amuser un peu avec les Data. La derni\u00e8re \u00e9tape est donc l'affichage. L'id\u00e9e est de cr\u00e9er une Windows Form qui contienne toutes ces informations dans un format beaucoup plus lisible et avec laquelle on pourrait interagir pour permettre de plus facilement commenter les Grands Prix. (exemple plus bas avec un croquis de ce \u00e0 quoi l'application pourrait ressembler) Voici la liste des donn\u00e9es qui pourraient \u00eatre affich\u00e9es (Non contractuel, simplement des id\u00e9es). Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand La moyenne de temps que les pilotes perdent dans les stands La performance moyenne des 5 types de pneus La moyenne de temps de chaque pilote sur le pneu actuel Le nombre de points que chaque pilote gagnerait selon sa position Le classement de la course Voire m\u00eame si c'est possible : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents Pr\u00e9dire les temps au tour de chaque pilote selon l'usure des pneus Voici un exemple d'interface possible pour une page : \"Proto\" Cas d'utilisation *On va consid\u00e9rer que tous les user ont un abonnement F1 TV PRO Un user veut r\u00e9cup\u00e9rer les data : Il ouvre son navigateur et lance la page DATA de la F1 TV Il calibre la capture des data via le programme (pour la premi\u00e8re utilisation). Il confirme que les donn\u00e9es initiales sont bonnes (pour la premi\u00e8re utilisation). Il regarde tranquille son Grand Prix Le programme r\u00e9cup\u00e8re les data : Il r\u00e9cup\u00e8re des images depuis la F1TV Il utilise Tesseract (ou autre) pour en r\u00e9cup\u00e9rer les infos. Il met ces infos dans un Objet Pilote, dans un Objet course avec un attribut tour pour hi\u00e9rarchiser les data Pour ce qui est de l'affichage, l'id\u00e9e est de faire une application C# comme on l'a appris \u00e0 l'\u00e9cole, mais avec assez de style pour qu'elle puisse \u00eatre agr\u00e9able \u00e0 utiliser. Quand le programme affiche les data : Il prend les donn\u00e9es venant directement de la F1TV. Il affiche diff\u00e9remment les donn\u00e9es pour permettre une meilleure lisibilit\u00e9 Il interpr\u00e8te avec des r\u00e8gles plut\u00f4t simples certaines data pour faire des minipr\u00e9dictions ou aider \u00e0 la lecture Il r\u00e9cup\u00e8re des infos d'autres courses pour les comparer et proposer des pr\u00e9dictions plus int\u00e9ressantes Difficult\u00e9s techniques R\u00e9cup\u00e9rer un flux vid\u00e9o plut\u00f4t propre malgr\u00e9 les contres mesures de la F1 TV pour en emp\u00eacher la lecture par un logiciel Si on doit passer par une capture d'\u00e9cran, trouver un moyen de stocker les donn\u00e9es de mani\u00e8re \u00e0 pr\u00e9voir que parfois un tour pourrait avoir plus de donn\u00e9es qu'un autre, que le user peut mettre pause, ou m\u00eame qu\u2019il revienne en arri\u00e8re. D\u00e9velopper des algorithmes pour r\u00e9cup\u00e9rer les donn\u00e9es comme les diff\u00e9rents pneus utilis\u00e9s ou l'activation du DRS ainsi que d\u00e9velopper des moyens de nettoyer les r\u00e9sultats de l'OCR (Par exemple utiliser diff\u00e9rents champs redondants pour comparer les r\u00e9sultats) Stocker les donn\u00e9es sur une base pour les traiter plus tard tout en pr\u00e9voyant un moyen de voir les stats live D\u00e9velopper des algorithmes de pr\u00e9diction qui prennent en compte d'anciennes courses pour tenter de pr\u00e9dire des choses comme les arr\u00eats aux stands par exemple.","title":"Cahier des charges"},{"location":"CahierDesCharges.html#cahier-des-charges","text":"Cahier des charges \"Track Trends\" Travail de dipl\u00f4me Maxime Rohmer 2023","title":"Cahier des charges"},{"location":"CahierDesCharges.html#contexte","text":"Je suis le \"Live Ticker\" charg\u00e9 de la Formule 1 pour le 20 minutes. On peut traduire cela comme commentateur de F1, avec tout de m\u00eame l'importante subtilit\u00e9 que je ne commente pas avec la voix, mais avec le clavier. Mes commentaires sont sous la forme de commentaires \u00e9crits live qui s'ajoutent au fur et \u00e0 mesure de l'\u00e9v\u00e8nement. Par exemple : \"Tour 28/54, Hamilton a fini par s'arr\u00eater et chausser des gommes tendres 13 tours apr\u00e8s Verstappen. L'Anglais va voir plus de 15 secondes \u00e0 rattraper, mais les gommes neuves et plus tendres que son rival devraient lui permettre s'il ne se fait pas trop ralentir par le trafic\". En g\u00e9n\u00e9ral avec un peu plus d'infos quand m\u00eame et cela tous les 3-4 tours Voici quelques exemples de pr\u00e9c\u00e9dents commentaires (Conseil : il y a un bouton pour montrer le feed dans l'ordre chronologique) : \"Commentaire Grand Prix de Belgique 2022\" \"Commentaire du Grand Prix de Singapour 2022\" \"Exemple commentaires\" Pendant un Grand Prix, je dois constamment : \u00c9crire ce qu'il se passe dans le grand prix et expliquer les enjeux Chercher r\u00e9guli\u00e8rement des m\u00e9dias \u00e0 inclure pour diversifier mon live (Tweets, Images etc.) Changer le titre et la description du live en fonction de l'\u00e9volution du Grand prix Et accessoirement regarder le grand prix pour y comprendre quelque chose Avec tout \u00e7a, il est tr\u00e8s difficile de garder un \u0153il sur la page DATA de la F1TV qui fournit pourtant des informations pr\u00e9cieuses. Je me retrouve parfois par exemple \u00e0 ne pas parler de d\u00e9passements dans le peloton, car ils ne sont pas retransmis \u00e0 la t\u00e9l\u00e9 alors que c'est une information importante. Autre exemple, occasionnellement le classement ne refl\u00e8te pas les vraies positions des pilotes. Les arr\u00eats aux stands font que du coup des pilotes qui devraient \u00eatre 15\u00e8mes se retrouvent 8\u1d49 puisqu'ils ne sont pas encore arr\u00eat\u00e9s. Cela peut de temps en temps pr\u00eater \u00e0 confusion.","title":"Contexte"},{"location":"CahierDesCharges.html#projet","text":"Un outil de style compagnon sous forme d'application C# Windows Form qui r\u00e9cup\u00e8re en temps r\u00e9el les informations de la course et affiche les informations les plus importantes. Le but est non seulement de faciliter mon job, mais aussi faire en sorte d'am\u00e9liorer la plus-value de mon travail en me permettant de fournir des commentaires qui ne sont pas disponibles pour le tout venant qui regarde simplement le flux RTS. Exemples: Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand Maintenant afficher diff\u00e9remment les infos, c'est sympa, mais cela serait encore mieux de traiter ces data et de permettre des petites pr\u00e9dictions. Exemples : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents","title":"Projet"},{"location":"CahierDesCharges.html#realisation","text":"Malheureusement, la Formula 1 Management ne propose aucune API publique qui puisse nous permettre de faire ce projet \"simplement\". La raison la plus probable \u00e9tant qu'Amazon avec son service AWS propose exactement ce genre de services pour le flux t\u00e9l\u00e9vis\u00e9 et il doit y avoir un contrat d'exclusivit\u00e9. Il existe des API \"Pirates\" faites par la communaut\u00e9, le probl\u00e8me est qu'elles ne sont pas forc\u00e9ment des plus pratiques \u00e0 utiliser. Mais comme je poss\u00e8de un abonnement premium ++ \u00e0 la F1TV, j'ai acc\u00e8s pour chaque grand prix \u00e0 un flux vid\u00e9o nomm\u00e9 : DATA F1 CHANNEL Qui ressemble \u00e0 \u00e7a : \"Data channel exemple\" Donc la seule fa\u00e7on que je vois de r\u00e9cup\u00e9rer ces donn\u00e9es est de les prendre directement sur ce feed. M\u00eame si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout \u00eatre la r\u00e9cup\u00e9ration des donn\u00e9es et leur stockage. Les donn\u00e9es viennent du flux vid\u00e9o et ainsi dans un premier temps, il va falloir r\u00e9cup\u00e9rer d'une mani\u00e8re ou d'une autre des images qui viennent d'un grand prix en direct ou en rediffusion. Ensuite, dans un second temps, il faut lire les informations directement sur l'image en utilisant une librairie pr\u00e9vue pour (exemple Tesseract) et v\u00e9rifier l'int\u00e9grit\u00e9 de ces derni\u00e8res pour qu'on puisse ensuite les stocker. Dans un troisi\u00e8me temps, il faut stocker toutes ces donn\u00e9es dans une forme qui permette d'aller facilement faire des requ\u00eates de r\u00e9cup\u00e9ration et d\u00e9j\u00e0 pr\u00e9parer des m\u00e9thodes qui permettent de r\u00e9cup\u00e9rer des infos importantes (ex : la moyenne des cinq derniers tours, le temps moyen d'arr\u00eat etc.) pour faciliter la derni\u00e8re \u00e9tape Quand tout cela est fait, on peut ensuite s'amuser un peu avec les Data. La derni\u00e8re \u00e9tape est donc l'affichage. L'id\u00e9e est de cr\u00e9er une Windows Form qui contienne toutes ces informations dans un format beaucoup plus lisible et avec laquelle on pourrait interagir pour permettre de plus facilement commenter les Grands Prix. (exemple plus bas avec un croquis de ce \u00e0 quoi l'application pourrait ressembler) Voici la liste des donn\u00e9es qui pourraient \u00eatre affich\u00e9es (Non contractuel, simplement des id\u00e9es). Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre). Les pilotes qui am\u00e9liorent leur temps au tour et ceux qui perdent le plus de temps Le classement pond\u00e9r\u00e9 tenant compte des futurs arr\u00eats au stand La moyenne de temps que les pilotes perdent dans les stands La performance moyenne des 5 types de pneus La moyenne de temps de chaque pilote sur le pneu actuel Le nombre de points que chaque pilote gagnerait selon sa position Le classement de la course Voire m\u00eame si c'est possible : Pr\u00e9dire les arr\u00eats aux stands en prenant en compte les baisses de performances des pneus Pr\u00e9dire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour Pr\u00e9dire dans combien de tour tel pilote va rattraper tel autre pilote Pr\u00e9dire combien de temps le pilote va perdre dans les stands en fonctions des arr\u00eats pr\u00e9c\u00e9dents Pr\u00e9dire les temps au tour de chaque pilote selon l'usure des pneus Voici un exemple d'interface possible pour une page : \"Proto\"","title":"R\u00e9alisation"},{"location":"CahierDesCharges.html#cas-dutilisation","text":"*On va consid\u00e9rer que tous les user ont un abonnement F1 TV PRO Un user veut r\u00e9cup\u00e9rer les data : Il ouvre son navigateur et lance la page DATA de la F1 TV Il calibre la capture des data via le programme (pour la premi\u00e8re utilisation). Il confirme que les donn\u00e9es initiales sont bonnes (pour la premi\u00e8re utilisation). Il regarde tranquille son Grand Prix Le programme r\u00e9cup\u00e8re les data : Il r\u00e9cup\u00e8re des images depuis la F1TV Il utilise Tesseract (ou autre) pour en r\u00e9cup\u00e9rer les infos. Il met ces infos dans un Objet Pilote, dans un Objet course avec un attribut tour pour hi\u00e9rarchiser les data Pour ce qui est de l'affichage, l'id\u00e9e est de faire une application C# comme on l'a appris \u00e0 l'\u00e9cole, mais avec assez de style pour qu'elle puisse \u00eatre agr\u00e9able \u00e0 utiliser. Quand le programme affiche les data : Il prend les donn\u00e9es venant directement de la F1TV. Il affiche diff\u00e9remment les donn\u00e9es pour permettre une meilleure lisibilit\u00e9 Il interpr\u00e8te avec des r\u00e8gles plut\u00f4t simples certaines data pour faire des minipr\u00e9dictions ou aider \u00e0 la lecture Il r\u00e9cup\u00e8re des infos d'autres courses pour les comparer et proposer des pr\u00e9dictions plus int\u00e9ressantes","title":"Cas d'utilisation"},{"location":"CahierDesCharges.html#difficultes-techniques","text":"R\u00e9cup\u00e9rer un flux vid\u00e9o plut\u00f4t propre malgr\u00e9 les contres mesures de la F1 TV pour en emp\u00eacher la lecture par un logiciel Si on doit passer par une capture d'\u00e9cran, trouver un moyen de stocker les donn\u00e9es de mani\u00e8re \u00e0 pr\u00e9voir que parfois un tour pourrait avoir plus de donn\u00e9es qu'un autre, que le user peut mettre pause, ou m\u00eame qu\u2019il revienne en arri\u00e8re. D\u00e9velopper des algorithmes pour r\u00e9cup\u00e9rer les donn\u00e9es comme les diff\u00e9rents pneus utilis\u00e9s ou l'activation du DRS ainsi que d\u00e9velopper des moyens de nettoyer les r\u00e9sultats de l'OCR (Par exemple utiliser diff\u00e9rents champs redondants pour comparer les r\u00e9sultats) Stocker les donn\u00e9es sur une base pour les traiter plus tard tout en pr\u00e9voyant un moyen de voir les stats live D\u00e9velopper des algorithmes de pr\u00e9diction qui prennent en compte d'anciennes courses pour tenter de pr\u00e9dire des choses comme les arr\u00eats aux stands par exemple.","title":"Difficult\u00e9s techniques"},{"location":"jdb.html","text":"Journal de bord Mercredi 29 Mars 2023 Premier jour du travail de dipl\u00f4me. Nous avons eu un briefing de mr Garcia et nous avons pu commencer \u00e0 pr\u00e9parer le travail. Nous avons eu les diff\u00e9rents fichiers nescessaires \u00e0 la bonne r\u00e9alisation du projet et je me suis mis \u00e0 faire les fichiers nescessaires La premi\u00e8re chose a \u00e9t\u00e9 de faire ce mkdocs dans lequel j'ai mis un fichier yml plut\u00f4t standart qui risque de changer au fur et \u00e0 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 \u00e0 quoi ca ressemble en forme de site \"Exemple mkdocs\" Ensuite il m'a fallu faire une version plus \u00e0 jour de mon cahier des charges car je n'y avait pas touch\u00e9 depuis novembre. J'ai envoy\u00e9 un mail \u00e0 mes enseignants pour qu'ils puissent y jeter un oeuil pour \u00eatre s\u00fbr que je n'ai rien chang\u00e9 qui les d\u00e9rangent. Monsieur Jayr m'a demad\u00e9 \u00e0 l'occasion de lui faire un planning type Gantt alors je me suis mis \u00e0 la t\u00e2che. J'ai fait un planning pr\u00e9visionnel et une l\u00e9gende les deux sont dispo dans le dossier planning de ce repertoire. Ensuite je me suis mis \u00e0 tout mettre sur git. A commencer par ce repertoire Et c'est deja la fin de la journ\u00e9e ! Demain j'avance un peu sur la doc avec ce que je peux d\u00e9ja remplir et je finis de pr\u00e9parer ce dont j'ai besoin pour commencer \u00e0 coder. Jeudi 30 Mars 2023 Aujourd'hui selon le planning je dois me charger des dernirers pr\u00e9paratifs pour commencer correctement. J'ai fait expr\u00e8s de prenre du temps pour ca au d\u00e9but pour ne pas me cr\u00e9er de soucis plus loin pendant le travail. Je vais envoyer par mail le planning que j'ai fait \u00e0 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 \u00e0 modifier quelques aspects au fur et \u00e0 mesure. J'ai aussi d\u00e9sactiv\u00e9 mkdocs with pdf par ce que les r\u00e9sultats ne sont vraiment pas ceux que j'attends et cela ralentis \u00e9norm\u00e9ment le d\u00e9ploiment. J'ai aussi rassembl\u00e9 mes croquis pour le poster : \"Croquis Poster 1\" \"Croquis Poster 2\" On peut voir que dans un premier temps j'ai tent\u00e9 de faire un poster un peu plus stylis\u00e9 et marketing. Cependant apr\u00e8s avoir discut\u00e9 avec Mr Garcia et diff\u00e9rents profs dont un de l'HEPIA et ils m'ont indiqu\u00e9 que ce qui \u00e9tait attendu \u00e9tait moins du marketing qu'un diagramme de fonctionnement. On peut voir sur les derniers posters que le cot\u00e9 technique ressort de plus en plus. Le but sera de faire une version encore plus technique ou on peut voir les diff\u00e9rents fonctionnements de l'application avec les technologies utilis\u00e9es. Le d\u00e9fi cela va \u00eatre de faire un joli poster qui soit en m\u00eame temps vendeur et en m\u00eame temps rempli techniquement. Oh et j'ai eu un probl\u00e8me ou mon calvier et ma souris ne voulaient d'un coup plus fonctionner. Dans mon cas c'\u00e9tait un probl\u00e8me de power management des ports. J'ai eu le soucis sur mon pc fixe \u00e0 la maison et sur mon pc portable \u00e9galement. En gros de ce que j'ai compris le soucis c'est que le pc croit que un port est trop solicit\u00e9 niveau puissance et du coup d\u00e9cide de couper l'alimentation du port USB. J'ai pu r\u00e8gler le soucis en allant dans le device manager sous universal bus controller sous power management et en d\u00e9cochant la case qui indique que windows peut d\u00e9sactiver ce port. Je ne conseille pas ce fix si vous avez des composants de mauvaise qualit\u00e9 car cela pourrait \u00eatre une vraie alerte cependant le fait que mes composants sont plut\u00f4t haut de gamme et le fait que mon clavier et ma souris le fassent en m\u00eame temps et que ils fonctionnaient tr\u00e8s bien depuis plus de 4 ans me font penser que c'est juste une nouvelle mise a jour de windows qui est p\u00e9nible. Demain je vais pouvoir commencer \u00e0 coder pour de bon. Vendredi 31/03/2023 Aujourd'hui on s'occupe de la PT2 qui est la programmation de la r\u00e9cup\u00e8ration 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 \u00e0 la reconnaissance. Je peux soit utiliser l'image crop\u00e9e directement : \"Image non trait\u00e9e\" Soit avec un filtre pour passer en noir et blanc laxiste \"Image trait\u00e9e laxiste\" Soit avec un filtre pour passer en noir et blanc stricte \"Image trait\u00e9e stricte\" Il va falloir faire des tests avec tous les noms et les chiffres pour trouver le plus efficace. Bon malheureusment Iron OCR semblait \u00eatre une bonne alternative mais c'est une librairie priv\u00e9e qui demande une license pour \u00eatre utilis\u00e9e. 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\u00f4t 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 \u00e9taient autrefois mal reconnus sont parfaitement interpr\u00eat\u00e9s. Par exemple voici un exemple de reconnaisance de texte sur tous les pilotes : \"Screenshot de reconnaisance d'image complete\" On voit que le nom Leclerc est mal reconnu. Mais voici ce que cela donne quand on prend une image qui ne contient que le nom Leclerc : \"Screenshot de reconnaissance d'image crop\u00e9e\" On voit ici que le nom Leclerc est tr\u00e8s bien reconnu. Dans le premier exemple on peut voir que Tsunoda est reconnu comme \"Reticin\" ce qui n'est pas exactement pareil (lol) Et quand on isole le nom Tsunoda dans une image seule : \"Screenshot de reconnaissance de Tsunoda\" Il le lit \"RETLELYY\" ce qui n'est toujours pas exactement ca... Une meilleure r\u00e9solution pourrait peut-\u00eatre r\u00e9soudre le probl\u00e8me en partie. Jusqu'ici les images \u00e9taient en presque 720P ce qui donne ceci : \"Tsunoda en 720P\" Et j'ai lanc\u00e9 une r\u00e9cup\u00e8ration d'images en 1080p pour r\u00e9cup\u00e8rer ceci : \"Tsunoda en 1080P\" On peut voir une certaine diff\u00e9rence tout de m\u00eame. Et quand on lance la reconnaissance : \"Reconnaissance Tsunoda en 1080P\" \"Tsunoda n'est plus \u00e9crit \"RETLELYY\" mais \"TSUNDDA\" ce qui n'est pas parfait mais qui est d\u00e9ja beaucoup mieux. J'ai essay\u00e9 de mettre l'engine de Tesseract en mode \"JPN\" comme Tsunoda est un nom japonais mais sans succ\u00e8s j'ai le m\u00eame r\u00e9sultat. Comme la r\u00e9solution est meilleure je me suis dit que peut \u00eatre le filtre de passage en noir et blanc pourrait aider. J'ai \u00e9crit 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\u00e9 avec un treshold de 100 et le programme a r\u00e9ussi \u00e0 me sortir Tsunoda en deux mots ce qui \u00e9tait d\u00e9ja tr\u00e8s encourageant. Et apr\u00e8s avoir augment\u00e9 le Treshold... Tada : \"Tsunoda 1080P avec filtre\" Le programme arrive bien \u00e0 reconnaitre TSUNODA. Je pense que cette tactique ne fonctionnait pas avant car la resolution \u00e9tait trop faible et l'aliasing se m\u00ealait trop avec le texte pour \u00eatre utilisable. Cependant cette technique ne fonctionne pas sur tous les noms. Par example avec Leclerc : \"Leclerc 1080P avec filtre\" On r\u00e9cup\u00e8re \"Leeler'c\" ce qui n'est pas bon du tout. Mais en modulant le Treshold (ici \u00e0 150) On peut de nouveau voir Leclerc \u00eatre reconnu correctement \"Leclerc 1080P avec filtre 2\" Je pense que pour avoir de bons r\u00e9sultats il va falloir faire un algo qui : D\u00e9coupe l'image en autant de plus petites images pour avoir un mot par image. Teste voir si avec l'image originale un nom correspond \u00e0 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\u00e9s. Seulement voila, il n'y a pas que des lettres que l'on veut r\u00e9cup\u00e8rer. On veut surtout pouvoir r\u00e9cup\u00e8rer les chiffres. Pour les chiffres on va avoir des soucis \u00e9galement... Si on essaie directement la m\u00eame technique sans filtre on a des r\u00e9sultats comme celui ci : \"Tentative de reconnaisance du timing\" La virgule a tendeance \u00e0 se barrer ce qui est particuli\u00e8rement probl\u00e9matique. Cependant comme les chiffres ont beaucoup moins de possibilit\u00e9es que les lettres et qu'il n'y a pas de probl\u00e8me de langue on devrait pouvoir travailler \u00e0 faire des r\u00e8glage que l'on pourra ensuite utiliser. Avec un Treshold de 165 on arrive presque \u00e0 quelque chose d'int\u00e9ressant : \"Tentative 2 de reconnaissance du timing\" Le + n'est clairement pas compris mais ca n'est pas emb\u00eatant car c'est souvent redondant. On arrive cependant \u00e0 isoler 3 et 259. M\u00eame si la virgule n'est pas comprise cela veut dire qu'il est tout de m\u00eame possible de discriminer les secondes des milisecondes. Maintenant avec un temps au tour : \"Reconnaissance du timing au tour\" On arrive sans rien changer aux param\u00eatres \u00e0 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 \u00e0 plus grande \u00e9chelle 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 \u00e0 quel point je peux faire confiance \u00e0 la reconnaissance des chiffres. Automatiser un syst\u00e8me de test de la sorte me sera tr\u00e8s utile dans le futur pour v\u00e9rifier 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\u00e9brouille 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\u00e9er un dataset qui permette de tester le programme de reconnaissance. Je pense que le meilleur moyen de faire serait un programme qui cr\u00e9e le dataset et qui ensuite peut tester diff\u00e9rentes methodes de reconnaissance. Par la m\u00eame occasion je peux d\u00e9velopper la technologie qui va permettre de d\u00e9couper 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\u00e9ja avoir une id\u00e9e de la structure de mon programme. Pour le moment je r\u00e9flechis \u00e0 un syst\u00e8me de \"Zones\" et de \"Windows\". L'id\u00e9e serait que une Zone est juste une sous partie d'image qui peut encore \u00eatre d\u00e9compos\u00e9 tandis que chaque Window contient une ou plusieurs informations \u00e0 r\u00e9cup\u00e8rer. J'ai essay\u00e9 de d\u00e9couper l'image pour que cela soit plus clair : \"Main zone\" Ici on peut voir que l'image est d\u00e9coup\u00e9e en plusieurs grandes zones. Dans un premier temps on ne s'occupe que de la premi\u00e8re. Ensuite : \"Driver zone #1\" On peut voir la que cette Main zone serait elle m\u00eame d\u00e9compos\u00e9e en plusieurs plus petites zones. Et ensuite chacunes de ces petites zones : \"Driver windows\" Sera d\u00e9compos\u00e9e 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\u00e9flechissant on pourrait tout \u00e0 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\u00e9e de : Une image de d\u00e9part Un rectangle qui la positionne sur cette derni\u00e8re Une liste de zones (potentiellement vide) Une liste de windows (potentiellement vide) Une methode qui permet de r\u00e9cup\u00e8rer une image de la zone Une methode qui permet de lancer la reconnaissance sur chaque window Une window serait compos\u00e9e de : Une image de d\u00e9part (cela peut \u00eatre l'image crop\u00e9e de la zone parente peu importe) Un rectangle qui la positionne sur cette derni\u00e8re Une methode qui permet de r\u00e9cup\u00e9rer un image de la window Une methode qui permet de lancer la reconnaisance sur l'image (Chaque type de zone doit l'impl\u00e9menter) 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\u00e9cup\u00e8rer dans une base de donn\u00e9e ou un objet. Par exemple une Zone de pilote pourrait tr\u00e8s bien contenir un objet pilote et le donner \u00e0 ses windows qui rempliraient ce m\u00eame objet. C'est une reflexion plus stockage que OCR mais c'est int\u00e9ressant pour savoir ce que fait une window des donn\u00e9es qu'elle r\u00e9cup\u00e8re. Dans un premier temps je pense que les windows vont simplement \u00e9crire dans un fichier ce qu'elles trouvent chacunes dans le format qu'elles veulent. Pour comprendre pourquoi je me prend la t\u00eate il faut savoir que chaque window peut avoir acc\u00e8s \u00e0 pleins d'informations diff\u00e9rentes. 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\u00e9ment pour un type de pneu ou un DRS ouver. Comme chaque window a plusieurs types de data elle devra elle m\u00eame se charger de comment la traiter ET de la stocker. Voila un diagramme qui r\u00e9sume comment je vois l'impl\u00e9mentation dans un premier temps : \"Diagramme d'explications\" Voici comment se pr\u00e9sente 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\u00e9er diff\u00e9rent types de Zones. Par exemple la MainZone devra d\u00e9couper son contenu en 20 parties \u00e9gales pour tenter de chopper les 20 pilotes. Il serait cool de trouver un moyen de calibrer automatiquement. C'est peut-\u00eatre 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-\u00eatre determiner des lignes. Et voici le squelette d'une window g\u00e9n\u00e9rique 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\u00eame impl\u00e9menter la r\u00e9cup\u00e8ration 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\u00e9rification 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\u00e9er une Main window qui se calibre toute seule. Alors apr\u00e8s avoir bien gal\u00e8r\u00e9 avec l'interface pour permettre au user de cliquer sur la form pour voir les zones qu'il cr\u00e9e, j'ai pu cr\u00e9er un zone qui fait les dimensions de MainZone et j'ai pu lancer la reconnaissance sur l'image et voir ou il trouve du texte : \"MainZone avec carr\u00e9s de texte\" Maintenant il faut que je nettoie la liste de rectangle pour exclure ceux qui sont trop grands pour \u00eatre sur une seule ligne, ceux qui indiquent le nombre de tour en haut et ceux qui n'ont pas d'int\u00e9r\u00eats. On pourra ensuite isoler les lignes et cr\u00e9er 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 \u00e0 l'utilisateur de ne pas les prendre dans la screenshot. Comme ils contiennent des mots qui peuvent \u00eatre utilis\u00e9s plus loin dans les data je ne peux pas les blacklister et faire un syst\u00e8me qui s'occupe de les enlever si ils existent selon le position y me prendrait trop de temps pour rien. Apr\u00e8s avoir filtr\u00e9 un peu les resultats et enlev\u00e9 les zones beaucoup trop grandes, on se retrouve d\u00e9ja plus qu'avec ca : \"MainZone avec de meilleurs carr\u00e9s\" Comme on peut le voir, du c\u00f4t\u00e9 gauche de l'image on a beaucoup de choses reconnues mais avec beaucoup de tailles diff\u00e9rentes ce qui n'est pas id\u00e9al. Alors j'ajoute un filtre qui permet de ne selectionner que les data sur la droite. \"MainZone avec de meilleurs carr\u00e9s\" Maintenant il devrait \u00eatre possible de faire un algorythme qui ne prend que un seul carr\u00e9 par ligne. \"MainZone avec un seul carr\u00e9 par ligne\" Maintenant que on sait ou se trouve chaque ligne on peut faire un petit traitement et d\u00e9couper l'image en plusieurs windows. Et voila : \"Mainzone auto calibr\u00e9e\" Maintenant le programme peut cr\u00e9er des zones pour chaque pilote \"Images pilotes\" \"Zone d'un pilote\" Maintenant il faut que j'impl\u00e9mente un syst\u00e8me un peu similaire pour cr\u00e9er des windows. Voici la methode que j'ai cr\u00e9\u00e9 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 \u00e9norme comme code mais pour tout mettre en place ca demande quand m\u00eame 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\u00e9 d'ajouter des windows sur une zone. J'ai juste quelques difficult\u00e9es \u00e0 les ajouter correctement, j'ai un offset tout pourri qui se met tout le temps \"Sainz coup\u00e9\" \"Perez coup\u00e9\" Cela doit \u00eatre un soucis lors de la detection de clic qui met un offset en trop. C'est vraiment p\u00e9nible 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 \u00eatre sauvegard\u00e9e comme ca pas besoin d'\u00e0 chaques fois le refaire. C'est bon ! J'avais juste oubli\u00e9 de changer le calcul d'offset entre le code de la zone et de la window. Note pour plus tard, il serait peut-\u00eatre judicieux de faire quelque chose pour la vue, les windows et les Zones ont le m\u00eame exact comportement pour la vue ce qui fait dupliquer du code. Mais au moins maintenant ca fonctionne : \"Ocr tester screenshot\" Et le programme va directement cr\u00e9er un dossier par pilote avec toutes les images de chaque Data le concernant : \"Dossier Perez\" ; Et c'est tout pour aujourd'hui je pense. Ce qui serait cool demain c'est que je puisse stocker d'une mani\u00e8re ou d'une autre ces fichiers de calibration et que je puisse les transf\u00e8rer vers le programme qui va s'occuper de d\u00e9coder et commencer gentillement \u00e0 d\u00e9coder les diff\u00e9rents types de data. Note pour quand je ferai les tests. Je pense que la meilleure id\u00e9e 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\u00e9codage. Pour le moment on est plut\u00f4t dans les clouts niveau planning. Mardi 4 Avril Aujourd'hui je suis scens\u00e9 plut\u00f4t bosser sur l'interpretation des donn\u00e9es, mais une id\u00e9e m'a taraud\u00e9 l'esprit toute la nuit. Est-ce que je ne pourrais pas quand m\u00eame essayer de d\u00e9composer la zone de pilote directment comme pour la Main zone. Pour ce faire j'ai tent\u00e9 de faire comme pour la main zone c'est \u00e0 dire lancer la reconnaissance pour savoir ou \u00e9taient tous les champs de donn\u00e9es mais malheureusement je ne pense pas que cela va \u00eatre possible. En effet non seulement ici les champs sont de tailles tr\u00e8s vari\u00e9es, mais en plus la reconnaissance n'arrive pas \u00e0 en r\u00e9cup\u00e8rer le m\u00eame nombre sur chaque ligne ce qui risque d'\u00eatre complexe \u00e0 utiliser ensuite. La preuve : \"Tentative d'auto calibration\" ; Cependant tout n'est pas perdu ! Il y a peut-\u00eatre 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-\u00eatre hard coder la valeur des diviseurs et les utiliser pour cr\u00e9er des zones. Ok alors visiblement c'est un probl\u00e8me car il semble y avoir d'autres pixels de cette couleur dans l'image (Qui l'aurait cru lol) \"Tentative 2\" J'a tent\u00e9 de r\u00e9duire la tol\u00e9rance mais le soucis c'est que c'est soit trop soit pas assez Derni\u00e8re tentative, j'ai essay\u00e9 de prendre plusieurs pixels en hauteur pour chaque incr\u00e9ment de X et en faire la moyenne, et m\u00eame comme ca, impossible de trouver de mani\u00e8re efficace les zones. Je pense que je vais donc revert tous mes changements pour revenir \u00e0 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\u00e9 une feature importante. Les zones font la largeur indiqu\u00e9e par l'utilisateur mais elles font la hauteur max comme ca toutes les window font la m\u00eame hauteur et ca permet \u00e0 l'utilisateur de ne pas forc\u00e9ment \u00eatre ultra pr\u00e9cis dans sa selection. Ce qui nous donne : \"Resultat final\" Maintenant je dirais que les deux prochaines choses \u00e0 faire seraient de stocker ces zones dans un fichier JSON ou autre pour que la calibration puisse \u00eatre envoy\u00e9e 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'\u00e9crant 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\u00e9 plutot que d'\u00e9crire les coordonn\u00e9es en dur pour chacunes. Chaque Grande zone va impl\u00e9menter 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\u00e9sultat auquel j'aimerais arriver. Mais pour y arriver il faut encore que je cr\u00e9e les diff\u00e9rents types de window. Cela veut dire que je dois d\u00e9cider quelles informations je vais r\u00e9cup\u00e8rer de la page. Par exemple je vais conserver la position du pilote mais au final les changements de positions sont difficiles \u00e0 lire et sont redondants. Si je garde un historique des positions des pilotes je peux calculer moi m\u00eame les changements. Pareil pour gap avec la voiture devant. Je pense que je vais juste garder l'information des \u00e9carts absolus et ensuite je pourrai toujours calculer la diff\u00e9rence entre les pilotes. Ca peut para\u00eetre b\u00eate car cela rajoute du calcul mais en r\u00e9alit\u00e9 le calcul de l'OCR est extr\u00eamement gourmand alors il faut que j'\u00e9vite le plus possible d'y faire recours. Il est bien plus rapide de calculer les \u00e9carts que d'essayer de reconnaitre le texte et le convertir en chiffre. J'ai visiblement ajout\u00e9 un bug dans mon code. Maintenant tous les pilotes ont la m\u00eame image quand on les selectionne. Mais visiblement ca n'\u00e9tait pas le cas avant car j'avais pu prendres des images de chaque pilote. J'ai pass\u00e9 3 minutes \u00e0 fixer un bug stupide j'ai un peu envie de br\u00fbler ma place de travail... Mais bon au moins maintenant cela fonctionne ! Toutes les images sont r\u00e9cup\u00e8r\u00e9es et ont un format correct avec le bon nom : \"Verstappen folder\" Avec un peu de code tr\u00e8s moche j'ai pu cr\u00e9er un fichier JSON qui contient les diff\u00e9rentes 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 \u00eatre possible de faire un fichier qui ne contient que les infos d'un seul pilote car ensuite il y a simplement un offset \u00e0 appliquer sur la zone et les windows. Je vais donc pouvoir commencer enfin le logiciel de d\u00e9codage qui prend en entr\u00e9e un fichier JSON comme celui ci qui a \u00e9t\u00e9 g\u00e9n\u00e8r\u00e9 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\u00e9rents pilotes pr\u00e9sents sur le Grand Prix et ce genre d'infos. Quoique je vais l'ajouter d\u00e9ja maintenant et plus tard je mettrai en place la feature acessible depuis l'interface. Mais le hardcoder maintenant me permet d\u00e9ja de mieux coder l'autre c\u00f4t\u00e9. Ce programme n'est en aucun cas termin\u00e9 et je vais devoir travailler encore un peu dessus pour qu'il soit utilisable correctement mais au moins il fonctionne \u00e0 peu pr\u00e8s. 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\u00e9codage. Demain je dois finir le d\u00e9codage du JSON et je dois commencer \u00e0 impl\u00e9menter la reconnaissance des textes. Voire m\u00eame 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 \u00e0 faire le programme de calibration mais je pense que c'est essentiel de prendre ce temps maintenant. (BTW il faudra quand m\u00eame retourner faire une plus jolie version par ce que la ca marche mais c'est tout) Bon apr\u00e8s pas mal de boulot je pense avoir r\u00e9ussi. Dans le nouveau programme on arrive \u00e0 r\u00e9cup\u00e8rer les diff\u00e9rentes zones : \"JSON decode result\" Un conseil de notre professeur M.Bonvin a \u00e9t\u00e9 de cr\u00e9er des Releases de versions qui ne fonctionnent pas ou pas tr\u00e8s bien. J'ai donc publi\u00e9 une premi\u00e8re release de l'OCR_TEST qui fonctionne vite fait. J'ai seulement un petit soucis, comme je recr\u00e9e compl\u00eatement la structure des driver zones avec seulement la premi\u00e8re, il y a un petit d\u00e9calage car entre les zones il y avait un gap. Ce qui fait que si la premi\u00e8re zone est parfaitement centr\u00e9e : \"Centered driver\" La vingti\u00e8me ne l'est plus exactement : \"Uncentered Driver\" Pour ca j'ai essay\u00e9 de mettre un espacement arbitraire mais c'est complexe. Je vais plut\u00f4t tenter de faire une diff\u00e9rence entre la taille de la zone compl\u00eate et de la taille additionn\u00e9e de toutes les fen\u00eatre et diviser le resultat entre toutes les fen\u00eatres. Ca n'est pas parfait mais au moins maintenant les donn\u00e9es ne touchent plus les bords de la fen\u00eatre. Et voila ! Maintenant avec le fichier de configuration en Json on arrive \u00e0 r\u00e9cup\u00e8rer toutes les infos comme si elles avaient \u00e9t\u00e9 envoy\u00e9es directement depuis l'app de calibration mais sans le processing time ! \"Verstappen folder 2 On peut donc ENFIN passer au d\u00e9codage de ces FICHUES donn\u00e9es. Je vais pouvoir impl\u00e9menter ce que j'ai fait dans le projet de test de d\u00e9codage. Gr\u00e2ce \u00e0 mon d\u00e9coupage initial qui m'a pris du temps \u00e0 impl\u00e9menter on a enfin un truc qui marche m\u00eame si je n'ai impl\u00e9ment\u00e9 que la reconnaissance de noms. \"Image reconnaissance propre\" Si on se rappelle du syst\u00e8me 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\u00fbrement un grand dataset qui prend des pilotes de reserves et des pilotes juniors pour que dans le cas ou un pilote est remplac\u00e9 dans l'ann\u00e9e il n'y a pas besoin de tout recalibrer avec l'application. Apr\u00e8s une discussion avec M.Bonvin j'ai d\u00e9cid\u00e9 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\u00e9rence entre deux strings. Bon et \u00e9videmment 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 \u00e0 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\u00eame pas id\u00e9al. Il va falloir que je trouve un moyen de le repond\u00e9rer. C'est dommage par ce que cela marche super avec Alonso Verstappen et Albon. J'ai un peu modifi\u00e9 la methode et j'ai fait en sorte d'envoyer tous les noms en majuscules en me disant que cela pourrait r\u00e9duire le nombre de changements. Et ca a march\u00e9 !! Cela va s\u00fbrement demander plus de tests pour \u00eatre bien s\u00fbr 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\u00e9ja fait une partie du boulot le premier jour alors je vais juste reprendre \u00e0 partir de l\u00e0. Je r\u00e9cup\u00e8re 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 \u00eatre trop dur \u00e0 g\u00e8rer. 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\u00e8s un peu de temps pour nettoyer la string etc on se retrouve avec un r\u00e9sultat 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\u00e9 car le laptime ne devrait pas \u00eatre 2:15 mais 1:35... Voila apr\u00e8s 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 \u00e0 devenir long, il serait peut-\u00eatre int\u00e9ressant d'utiliser un seul Tesseract Engine ou de voir ce qui prend autant de temps, on d\u00e9passe la seconde de traitement ce qui est un peu ma limite. Apr\u00e8s on peut toujours tester de rajouter du multicore processing mais c'est pour une autre fois. Demain je m'occupe de r\u00e8gler les soucis que j'ai avec la prescision de ces temps au tour et j'\u00e9sp\u00e8re pouvoir m'occuper aussi de la position des pneus et du DRS. J'aimerais finir tout ca cette semaine. Jeudi 6 Avril Une id\u00e9e m'est pass\u00e9e par la t\u00eate pendant que je dormais, dans la liste des pilotes, quand ils sont \u00e0 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 \u00e9videmment un probl\u00e8me. Je pense qu'une bonne facon d'envoyer l'info serait de retourner -1 -2etc... \u00e0 la place des milisecondes, mais encore faut-il detecter le nombre de tours Je devrais \u00eatre 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\u00e9cup\u00e8r\u00e9 toutes les infos au moins un peu proprement. Cela veut dire que je commence officiellement \u00e0 prendre du retard. (Sachant que si je finis tout aujourd'hui une journ\u00e9e de doc suffira largement le terme est un peu exag\u00e8r\u00e9 mais bon) Bon pour la reconnaissance des temps c'est sp\u00e9cial... Le filtre semble ne pas changer grand chose ce qui est probl\u00e9matique et ca n'est vraiment pas fiable. Voici quelques expemples avec un treshold de 100: \"11ZSD\" Cette image est comprise comme \"11ZSD\" 42340 Cette image est comprise comme \"42340\" \"ZZAEB\" Et celle ci \"ZZAEB\"... Ce qui... n'est pas bon du tout... J'ai essay\u00e9 de trouver un fichier d'entrainement sp\u00e9cifiquement fait pour les digits. J'ai essay\u00e9 de blacklister les chars non voulus pour tenter d'obliger Tesseract \u00e0 trouver des chiffres. Avec la premi\u00e8re option, les r\u00e9sultats ne sont pas meilleurs voire pires. Avec la seconde option c'est d\u00e9ja pas mal mieux mais on perd compl\u00e8tement la possibilit\u00e9 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\u00e9es 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\u00e9 d'utiliser de l'interpolation our augmenter la taille de l'image et ensuite appliquer mon filtre pour retirer le flou mais sans succ\u00e8s... Pourtant la on se retrouve avec des images plut\u00f4t claires : \"Clear1\" Ici le programme trouve \"44301\" \"Clear2\" Et ici \"A5151\"... On a toujours les m\u00eames probl\u00e8mes. Bon je suis all\u00e9 me renseigner sur l'OCR et je me suis dit que j'allais tenter de faire les choses proprement. Je vais faire passer plusieurs \u00e9tapes de postProcessing avant de donner l'image \u00e0 Tesseract. GrayScale Tresholding InvertColors Scaling Dilatation Ce qui donne : \"Original\" \"Grayscale\" \"InvertColors\" ; \"Resize\" ; \"Dilatation\" Ce qui ne change : Roulement de tambour RIEN kjd viuwvuirnvoirenbf Tout ca pour rien... C'EST BON !!! Bon en fait au final le probl\u00e8me \u00e9tait une mauvaise configuration de Tesseract. Je vais devoir un peu nettoyer tout ca. Mais avec les changements de l'image on a des r\u00e9sultats BEAUCOUP plus pr\u00e9cis et potentiellement utilisables. La je vais devoir faire un serieux travail de nettoyage et simplification de mon code par ce que la c'est vraiment un chantier vu le nombre de choses que j'ai du essayer. J'ai du aussi beaucoup modifier la gestion de l'image ce qui donne : \"Clean\" Et la on a des r\u00e9sultats 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\u00e9mentation de la reconnaissance du DRS et des Pneus Et non... je viens de me rendre compte que mon programme a encore cass\u00e9 car le tap time ne peut pas \u00eatre 23 min lol. J'ai un nouveau magnifique probl\u00e8me... Les points et les deux points sont interpr\u00eat\u00e9s comme des chiffres ... Give me a F * * break... J'ai du mal \u00e0 comprendre pourquoi ils ne sont d\u00e9tect\u00e9s comme tels que maintenant. Bon alors il semblerait les temps au tour aie besoin d'un ordre tr\u00e8s pr\u00e9cis pour fonctionner. Grayscale InvertColors Tresholding Resize * 2 Resize * 2 Et la on a des r\u00e9sultats un peu mieux. Bon demain il faut absolument que je me charge de r\u00e8gler tous ces probl\u00e8mes et que je commence la reconnaissance des pneus et de DRS par ce que je commence \u00e0 \u00eatre en retard. Vendredi 6 Avril 2023 Alors aujourd'hui c'est le dernier jour avant de commencer \u00e0 \u00eatre en retard pour de bon. J'ai r\u00e9ussi \u00e0 r\u00e8gler le probl\u00e8me des temps au tour, des gaps, et des secteurs. Dans le processus j'ai cass\u00e9 la detection de position mais ca devrait pas \u00eatre TROP compliqu\u00e9. Et voila ... Apr\u00e8s seulement plus de dix heures de gal\u00e8re, 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\u00f4t bien pour le DRS est que je prend la premi\u00e8re image de DRS et je la d\u00e9clare comme valeur \u00e9talon 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 \u00e0 moins de deux secondes les uns des autres pour confirmer). Ensuite cette valeur \u00e9talon 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\u00e9 ex: Ceci est un DRS ferm\u00e9: \"Closed DRS\" Ceci est un DRS ouvert: \"Open DRS\" Cela marche \u00e0 peu pr\u00e8s 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\u00eatant d'utiliser GetPixel et SetPixel qui sont simplement des horreurs \u00e0 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 \u00e9volue tout le long du Grand Prix. Bref c'est la gal\u00e8re. En y r\u00e9flechissant je me suis dit qu'une bonne id\u00e9e pourrait \u00eatre de partir de la droite de la zone du pneu en regardant au milieu de la hauteur. Puis continuer vers la gauche jusqu'\u00e0 ce que je rencontre une couleur diff\u00e9rente. 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\u00e9termin\u00e9 que le background n'\u00e9tait jamais plus clair que #505050 et que donc nimporte quelle couleur qui aurait plus que 50 dans un seul des channels serait consid\u00e8r\u00e9e comme une couleur cassant le background Pour arriver \u00e0 cette conclusion je me suis amus\u00e9 un peu avec les couleurs pour jouer avec les limites de mon algorythme : \"Color fun\" Et je crois que j'ai eu une bonne id\u00e9e, avec une petite methode bien faite on arrive \u00e0 de supers r\u00e9sultats : private Rectangle FindTyreZone() { Bitmap bmp = WindowImage; int currentPosition = bmp.Width; int height = bmp.Height / 2; Color limitColor = Color.FromArgb(0x50,0x50,0x50); Color currentColor = Color.FromArgb(0,0,0); Size newWindowSize = new Size(bmp.Height,bmp.Height); while(currentColor.R <= limitColor.R && currentColor.G <= limitColor.G && currentColor.B <= limitColor.B && currentPosition > 0) { currentPosition--; currentColor = bmp.GetPixel(currentPosition,height); } //Its here to let the new window include a little bit of the right side int offset = Convert.ToInt32((float)newWindowSize.Width / 100f * 20f); int CorrectedX = currentPosition - (newWindowSize.Width - offset); if (CorrectedX <= 0) return new Rectangle(0,0,newWindowSize.Width,newWindowSize.Height); return new Rectangle(CorrectedX,0,newWindowSize.Width,newWindowSize.Height); } \"Tyres\" Maintenant cela devrait \u00eatre beaucoup plus simple de trouver la couleur g\u00e9n\u00e9rale et le nombre de tours. Donc ce que je fais c'est que je fais une reconnaissance de texte sur l'image r\u00e9duite. 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\u00e9rentes couleurs de pneus : SOFT : #FF0000 MEDIUM : #f5bf00 HARD : #d9d8d4 INTER : #00a42e WET : #2760a6 \"Tyre colors\" Les couleurs de pneus peuvent changer de temps \u00e0 autres, par exemple cette r\u00e8gle de pneus est arriv\u00e9e en 2019 et avant il y avait beaucoup plus de couleurs mais dans une volont\u00e9 de rendre le sport plus facile \u00e0 comprendre \u00e0 la t\u00e9l\u00e9 cela a \u00e9t\u00e9 simplifi\u00e9. Je ne pense pas que cela va changer dans les ann\u00e9es qui viennent alors tout est hardcod\u00e9. Je pense que j'ai des soucis avec la detection de texte et de couleur car ma zone est trop grande. Alors bon j'\u00e9crit ces lignes apres des heures de tests. Il semble que la principale difficult\u00e9 avec ces pneus c'est que les chiffres ou lettres sont minuscules. Il est donc extr\u00eamement difficile de faire une reconnaissance ne serait-ce qu'un peu fiable.. Je fais de mon mieux pour tenter de r\u00e8gler le soucis cependant c'est vraiment complexe. Je commence \u00e0 devenir fou, je tente tout et nimporte quoi pour permettre \u00e0 mon algo de fonctionner et m\u00eame quand je fais du post processing comme pas possible il me retourne toujours nimporte quoi... \"5i t'inqui\u00e8tes\" 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\u00e9flechir 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\u00f4t\u00e9 la detection de chiffres pour me pencher un peu plus sur la d\u00e9tection de couleur. Par ce que techniquement si j'arrive \u00e0 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\u00e9ussi \u00e0 fix mon probl\u00e8me de mauvaise detection de couleur de pneus. Du moins je crois. Seulement j'ai quand m\u00eame un souci, les fen\u00eatres de pneus avec une lettre n'ont pas assez de couleur pour \u00eatre d\u00e9tect\u00e9s. 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\u00e9duire les possibilit\u00e9s de Tesseract. Je me rend de plus en plus compte que le plus important c'est de r\u00e9duire 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\u00eat\u00e9 comme trois I ou comme un M... J'en ai marre sans rire c'est vraiment p\u00e9nible. Alors j'\u00e9crit ces lignes deux jours plus tard et me rend compte avec horreur que toutes mes modifications sur ce journal de bord n'ont pas \u00e9t\u00e9 auvegard\u00e9e... yess.. Bon pour faire simple, j'ai r\u00e9ussi \u00e0 rendre la detection de couleurs bien plus efficace en r\u00e9duisant la taille de l'image et en ne prenant pas en compte les couleurs que l'on d\u00e9tecte comme \u00e9tant partie int\u00e9grante du background. Par exemple quand on a une image comme celle ci : \"Avec background\" qui contient un background alors que ci dessous, on l'a enlev\u00e9. \"Sans background\" La diff\u00e9rence est t\u00e9nue mais elle permet de grandement am\u00e9liorer la prescision de la reconnaissance de couleurs. Pour ce qui est du nombre de tours je me suis rendu compte que cela n'\u00e9tait d\u00e9ja pas tr\u00e8s utile car avec l'historique on devrait pouvoir le d\u00e9duire. Mais bon pour la forme je me suis dit que cela serait quand m\u00eame une bonne id\u00e9e de v\u00e9rifier avec la reconnaissance. J'\u00e9tais quasi certain que le soucis \u00e9tait 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 \u00e9t\u00e9 simple) J'ai pu avoir des chiffres beaucoup plus proches de la r\u00e9alit\u00e9. En m\u00eame temps je ne vois pas bien comment j'aurais pu faire mieux : \"Super 11\" Je suis quand m\u00eame assez fier de voir que j'ai r\u00e9ussi \u00e0 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\u00e9zoomer un peu et c'est avec ca que les lettres ont pu \u00eatre mieux reconnues : \"Super H\" \"Super M\" Maintenant je pense qu'il ne reste \"plus qu'\u00e0\" nettoyer un peu tout ce code qui traine de partout pour tout faire fonctionner et impl\u00e9menter 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'\u00e9t\u00e2t, le programme met 25 secondes \u00e0 d\u00e9marrer et consomme presque 2GB de Ram. Certes cela ne veut pas dire que la reconnaissance \u00e0 elle seule prend 25 secondes car au d\u00e9marrage il y a aussi la lecture du fichier de config et la cr\u00e9ation des window etc.. En r\u00e9alit\u00e9 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 \u00eatre possible mais en tout cas le but va \u00eatre de s'en rapprocher. Pour \u00eatre 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 \u00e0 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\u00e9marrage 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\u00e9s 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\u00e9 d'utiliser un Parrallel.For au moment de la cr\u00e9ation des windows, le probl\u00e8me c'est que visiblement les objets windows sont beaucoup trops complexes et utilisent trop de ressources partag\u00e9es pour \u00eatre vraiment thread Safe. J'\u00e9sp\u00e8re que je n'aurais pas trop de soucis avec ca qu'en j'en viendrai \u00e0 l'optimisation de l'OCR... Ce qui me rend fou c'est que cette boucle toute nulle prend plus de dix secondes \u00e0 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 \u00eatre ca qui prend du temps. Et les windows que je cr\u00e9\u00e9 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\u00e9cile... J'ai juste \u00e0 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\u00e8s belle am\u00e9lioration pour pas tr\u00e8s ch\u00e8r mais bon c'est un peu b\u00eate... Bon je pense que 2-300ms c'est une dur\u00e9e correcte surtout que ca n'est appel\u00e9 qu'une fois pour le lancement. On peut passer \u00e0 la suite maintenant. Alors il y a un grand soucis avec la parallellisation de l'OCR... Tesseract n'est pas par d\u00e9faut une classe \"Thread safe\" ce qui veut dire que je ne peut utiliser de parallell.Foreach sur mes windows pour acc\u00e8l\u00e8rer 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 \u00e0 vue, je me base sur diff\u00e9rentes infos que je trouve sur des sites un peu perdus et sur chatGPT, j'esp\u00e8re que j'arriverai \u00e0 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 \u00e0 la fois. Je vais donc retirer l'engine unique pour voir si en cr\u00e9er un par window me permet de passer en multithreading. La grande question sera : Est-ce que les ressources suppl\u00e9mentaires que vont prendre la cr\u00e9ation de tous ces engines va compenser enti\u00e8rement le temps gagn\u00e9 avec la paralellisation. Pour stocker les donn\u00e9es dans un premier temps je vais cr\u00e9er un objet DriverData. Ce qu'il y a de pratique avec ca, c'est que je pourrais ajouter du code de v\u00e9rification de certaines donn\u00e9es directement dedans avant de les donner \u00e0 la suite du programme. Et on peut m\u00eame imaginer une impl\u00e9mentation d'une liste de DriverData pour avoir l'historique. Ce qui serait cool ca serait de grouper toutes ces data avec un num\u00e9ro 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\u00e9 sur la page de la F1TV n'est accompli que par certains des premiers pilotes. D'autres pilotes peuvent \u00eatre dans des tours pr\u00e9c\u00e9dents si ils ont du retard. Il faudra r\u00e9fl\u00e9chir \u00e0 cela quand je viendrai au mod\u00e8le. Bon pour y arriver j'ai du faire de gros changements et le r\u00e9sultat n'est peut-\u00eatre pas aussi cool que ce que j'aurais voulut... Voici un petit point sur les performances maintenant J'ai \u00e9galement d\u00e9sactiv\u00e9 le dump d'images. Pour le moment j'ai tout mis en commentaire mais cela pourrait \u00eatre int\u00e9ressant de faire en sorte de pouvoir l'activer en changeant une ou deux variables Au d\u00e9marrage : Loading - 113ms Splitting d'images - 14ms Ocr - 7s En passant d'une image \u00e0 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\u00e9ation d'engines d'\u00eatre \u00e0 l'origine de ces performances presque d\u00e9cevantes. 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\u00e9 de mettre un stopwatch sur une des cr\u00e9ations d'engine Tesseract et le r\u00e9sultat me parait fou... Plus d'une seconde c'est dingue. J'ai test\u00e9 dans d'autres endroits du code et effectivement il semble que la cr\u00e9ation d'un engine prenne entre une et deux secondes ce qui est une ETERNITEE what ! Donc il faut optimiser tout ca. Une id\u00e9e serait de d\u00e9composer 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\u00e9\u00e9s ils ne prennent pas de temps du tout. Cr\u00e9er une fois tous les engines et ensuite les utiliser pourrait \u00eatre une bonne id\u00e9e. Cela prendrait longtemps au load mais ensuite les reconnaissances devraient \u00eatre super rapides. Ok alors ca c'est d\u00e9ja plus ce \u00e0 quoi je m'attendais ! On est de nouveau \u00e0 plus de 10s de loading time mais on est descendu \u00e0 deux secondes par OCR. (Bon autre soucis, l'utilisation de la RAM est ridicule plus de 2gb mais ce qui m'inqui\u00e8te c'est que j'ai l'impression qu'elle augmente plus on change d'image) J'ai r\u00e8gl\u00e9 (en partie) le soucis en obligeant le GC (Garbage Collector) \u00e0 collecter apr\u00e8s chaque detection. m\u00eame apr\u00e8s 50 detections l'utilisation de la ram se stabilize autour des 2GB. Bon en paralellisant la cr\u00e9ation des Engines le soucis c'est que cela demande d'allouer beaucoup trop de m\u00e9moire d'un coup alors le programme se fige pendant genre cinq secondes avant de tout cr\u00e9er. Du coup m\u00eame si la cr\u00e9ation 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\u00e9ale... J'ai r\u00e9ussi \u00e0 changer un petit peu le programme de reconnaissance pour rendre la reconnaissance un peu meilleure mais cela a drastiquement augment\u00e9 le temps requis pour d\u00e9coder... On arrive \u00e0 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 \u00e0 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\u00eater la pour la partie d\u00e9codage. 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 \u00e0 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\u00e9ressante du projet et ou je pense que j'aurai le plus essay\u00e9 de choses qui vallent le coup d\u00eatres racont\u00e9es. J'ai aussi pass\u00e9 pas mal de temps sur le poster du projet. J'avais fait des croquis au crayon de ce \u00e0 quoi je pensais, cependant apr\u00e8s de longues discussions avec M.Garcia ils n'\u00e9taient pas forc\u00e9ment tr\u00e8s bons car ils ne repr\u00e9sentent pas assez bien le fonctionnement du projet et sont un peu trop marketings. Du coup j'ai fait une premi\u00e8re version au propre : \"Poster V1\" Mais je n'\u00e9tais pas forc\u00e9ment content du r\u00e9sultat et il manquait des choses je trouve comme par exemple l'utilisation de Selenium. J'ai donc repass\u00e9 des heures \u00e0 faire une seconde version : \"Poster V2\" La police d'\u00e9criture n'est pas encore la bonne mais cela va venir. Mais je pr\u00e9f\u00e8re d\u00e9ja beaucoup cette version \u00e0 la premi\u00e8re. Je ne sais pas encore si la version finale sera une version plus travaill\u00e9e de ce poster ou compl\u00eatement autre chose mais pour l'instant je suis \u00e0 peu pr\u00e8s content de cette version. Je le trouve un tout petit peu trop brouillon ou avec trop d'infos mais il m'a \u00e9t\u00e9 de nombreuses fois reproch\u00e9 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 \u00e0 peu pr\u00e8s 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\u00e9\u00e9 u nouveau projet selenium mais m\u00eame avec les bonnes libraries je n'arrivais pas \u00e0 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\u00e8gler le probl\u00e8me en t\u00e9l\u00e9chargeant 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\u00e8me c'est que du coup il faut tout le temps d\u00e9placer 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\u00e9cup\u00e9ration de cookie. J'ai d\u00e9ja 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 \u00e9cran et un programme qui prend des captures d'\u00e9crans. Avoir une cam\u00e9ra qui prend en photo l'\u00e9cran au cas ou chrome et Firefox emp\u00eachent la prise de captures d'\u00e9crans. R\u00e9cup\u00e8rer directement le feed en faisant du reverse engeneering de la plateforme. Simuler un chrome en background qui prenne des screenshot sans qu'on aie \u00e0 le voir. Dans toutes ces options, je dirais que la pire \u00e9tait celle de la cam\u00e9ra qui filme l'\u00e9cran, mais \u00e0 l'\u00e9poque c'\u00e9tait 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\u00e8s mauvaise qualit\u00e9. Une autre option qui m'aurait vraiment emb\u00eat\u00e9 aurait \u00e9t\u00e9 de devoir garder une page de Chrome ou Firefox ouverte quelque part sur un \u00e9cran pour que le programme puisse prendres des captures d'\u00e9crans. C'est de loin l'option la plus simple et la plus logique mais elle poss\u00e8de pour moi de tr\u00e8s gros points noirs : On ne peut pas certifier l'int\u00e9grit\u00e9 des donn\u00e9es car l'utilisateur a le contr\u00f4le total sur le feed. Il peut mettre pause, avancer, reculer, tout casser sans faire expr\u00e8s 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'\u00e9cran 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\u00e8tre d'\u00e9cran est crucia\u00e9 ! Alors avoir un \u00e9cran complet bloqu\u00e9 est juste un point bloquant qui m'emp\u00eacherait d'utiliser l'app aussi bonne soit-elle dans ses pr\u00e9dictions. Mais bon si aucune autre methode ne fonctionne ce qui est bien c'est que celle la est plut\u00f4t simple \u00e0 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\u00e9gal haha. L'id\u00e9e serait de r\u00e9cup\u00e8rer le lien vers le broadcast g\u00e9n\u00e9ral et de comprendre comment il fonctionne pour le d\u00e9coder nous m\u00eame 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\u00e9riodes ou je travaille en plus) Difficile de faire un syst\u00e8me qui marche pareil pour les rediffusions et les lives. (En effet les liens des rediff sont beaucoup plus simple \u00e0 r\u00e9cup\u00e8rer 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-\u00eatre que avec des profs qui m'aident et chat gpt ainsi qu'internet je pourrais potentiellement n\u00e9gocier un truc mais c'est hautement improbable et cela serait une perte de temps folle si je n'y arrive pas. Derni\u00e8re option que je trouve la plus s\u00e9duisante. Simuler une instance de Chrome ou de Firefox (Le soucis avec chrome c'est qu'il impl\u00e9mente l'utilisation de DRM dans les vid\u00e9os qui fait qu'il est tr\u00e8s difficile de passer outre la s\u00e9curit\u00e9 avec un bot) pour ensuite prendre des captures d'\u00e9crans automatiquement. Cette solutions offre pleins d'avantages : Pas de place prise sur l'\u00e9cran L'int\u00e9grit\u00e9 des donn\u00e9es est assur\u00e9e car c'est le programme qui d\u00e9cide 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\u00e9matiques : Comment se connecter automatiquement sans \u00eatre detect\u00e9 par un Bot et sans demander \u00e0 l'utilisateur ses identifiants (Pour des raisons \u00e9videntes 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\u00e9 et en plein \u00e9cran. Mais j'ai d\u00e9cid\u00e9 de partir sur cette option. Pour ce faire j'utilise Selenium. J'ai pu tester Puppetteer Sharp et m\u00eame 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\u00e9cid\u00e9 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\u00e9sactive le \"Headless\" pour qu'on puisse voir ce que fait l'app car sinon tout est invisible. Alors dans les faits la vid\u00e9o 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\u00e9rents challenges que l'on va devoir surmonter pour vraiment faire ce que l'on veut. Mais un petit d\u00e9tail extr\u00eamement important, la F1TV est un programme payant un peu comme netflix. Ce qui veut dire que pour acc\u00e8der au contenu il faut \u00eatre connect\u00e9. Sauf que une instance de firefox cr\u00e9\u00e9 par Selenium est comme une page de naviguation priv\u00e9e, ce qui veut dire que si on va sur la page de la F1TV on est pas connect\u00e9s. Je pourrais tout \u00e0 fait demander \u00e0 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 \u00e9t\u00e9 prot\u00e8g\u00e9e avec la meilleure technologie de detection de bots que je connaisse. Presque aucun site n'arrive \u00e0 me detecter sauf eux ! Donc c'est tout simplement impossible d'utiliser cette technique. Ensuite je me suis rappel\u00e9 que ce que la page stocke pour me permettre de rester connect\u00e9 ce sont des cookies. Et si je mets le bon cookies dans Selenium alors je serai connect\u00e9. Dans un premier temps je voulais faire un syst\u00e8me 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\u00e9cup\u00e8ration de cookies ! Tous les cookies de chrome sont stock\u00e9s dans une base de donn\u00e9es SQLITE. On pourrait se dire Banco il suffit d'aller dedans et de retrouver tous les cookies et se connecter. Sauf que, pas b\u00eates, les \u00e9quipes de chrome ont d\u00e9cid\u00e9 que c'\u00e9tait une bonne id\u00e9e 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\u00e9coder. Mon hypoth\u00e8se a \u00e9t\u00e9 que si ces cookies peuvent \u00eatre lus par Chrome m\u00eame hors connexion, c'est que la cl\u00e9 de d\u00e9codage existe sur l'appareil et qu'il suffit de la trouver. ET C'EST LE CAS! Apr\u00e8s pas mal de recherches j'ai pu voir que la cl\u00e9 de d\u00e9codage existe bel et bien et qu'il suffit de la d\u00e9coder en utilisant la librairie DPAPI pour la lire. Avec cette cl\u00e9 on peut ensuite d\u00e9coder les cookies et leurs valeurs ce qui veut dire qu'il est th\u00e9oriquement possible d'automatiser le processus sans que l'utilisateur n'aie rien \u00e0 faire. J'ai d\u00e9cid\u00e9 de faire la partie r\u00e9cup\u00e8ration en python pour deux raison : Je n'arrivais pas \u00e0 trouver une bonne impl\u00e9mentation de DPAPI en C# qui me permettait de d\u00e9coder la cl\u00e9. 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 \u00e0 recompiler le code C#. J'ai donc avec l'aide d'internet et de ChatGPT cr\u00e9\u00e9 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\u00e9 et va ensuite stocker les r\u00e9sultats dans un CSV pour qu'il soit facile d'y acc\u00e8der depuis le C#. Alors oui cela pose certaines questions de s\u00e9curit\u00e9. Car en effet je prend tous les cookies, les d\u00e9code et les stocke. Ce qui veut dire que je pourrais tout \u00e0 fait envoyer ces donn\u00e9es 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\u00e9 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\u00f4t\u00e9 j'ai juste \u00e0 lire le CSV et le tour est jou\u00e9 ! (Trouver cette solution m'a pris une semaine de vacances \u00e0 l'\u00e9poque) Bon j'ai r\u00e9ussi \u00e0 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\u00e9. Pour changer la qualit\u00e9 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\u00eame. Elle commence toujours pas \"1080_\" mais ensuite ca peut \u00eatre \"1080_45930285\" ou \"1080_56801\" la suite est apparemment random. J'ai donc du utiliser ce code pour le selectioner quand m\u00eame : 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\u00e8me c'est qu'il est invisible alors on doit le faire apparaitre. J'ai tent\u00e9 de le faire aparaitre en bougeant la souris, en cliquant \u00e0 un endroit pr\u00e9cis, impossible de le faire marcher correctement. Puis j'ai eu l'id\u00e9e de mettre pause en envoyant un appui sur la touche Espace et ca a permit de d\u00e9couvrir le bouton et permettre qu'on clique dessus. Ca peut paraitre tout b\u00eate mais rien que ca, ca m'a pris un temps consid\u00e9rable. Bon pour ce qui est du timecode de la vid\u00e9o. Je pense qu'il serait trop complexe de faire en sorte que selenium change le slider de progression de la vid\u00e9o. Alors j'ai fait quelques tests et apparemment, si on quitte la F1TV sur un timecode de la vid\u00e9o que on donne au programme, comme il r\u00e9cup\u00e8re tous les cookies de la F1TV il commencera de la. Donc si on veut utiliser le programme avec des Grand Prix ayant d\u00e9ja eu lieu, on peut le faire, seulement il faudra juste au pr\u00e9alable avoir choisit le bon timecode dans le page de la F1TV avant de le lancer. Ce qui est int\u00e9ressant c'est que la page de la F1TV ressemble \u00e0 ca au d\u00e9part : \"Empty F1TV\" Je pense qu'une bonne id\u00e9e serait de dire au programme que c'est la grille de d\u00e9part et ensuite d\u00e8s qu'il d\u00e9tecte un secteur il sait que la course a commenc\u00e9. Lundi 24 Avril 2023 Aujourd'hui c'est jour de documentation. J'ai pas mal travaill\u00e9 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-\u00eatre un peu demain. Ok normalement je ne devrais faire que de la documentation mais je ne peux pas passer \u00e0 cot\u00e9 de ca... Le probl\u00e8me que j'ai avec les pneus ou parfois il d\u00e9tecte un H au lieu d'un '11' et ce genre de choses c'est \u00e0 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\u00eame et qui va donc defigurer les 11 : \"diformed 11\" \"diformed 11\" J'ai r\u00e9ussi \u00e0 les changer en : \"less diformed\" \"less diformed\" Mais au final cela n'a pas augment\u00e9 la pr\u00e9cision 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\u00eater de chercher des lettres ou des chiffres. Comme ca les 11 arr\u00eateront d'\u00eatre 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\u00eame si il y a une lettre sur fond noir pour d\u00e9crire le pneu, mon methode de r\u00e9cup\u00e8ration de la couleur dominante ommet les pixels trop noirs alors il est quand m\u00eame possible de determiner le type de pneus. Et tout simplement si il n'arrive pas \u00e0 lire le chiffre c'est que c'est une lettre et que donc on est \u00e0 0 tours. Cela marche plut\u00f4t bien et cela simplifie pas mal le processing. Voila, la je vais me remettre \u00e0 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\u00eame temps en documentant je vois des choses que j'ai soit mal fait soit que je pourrais faire mieux en changeant tr\u00e8s peu de choses. J'\u00e9sp\u00e8re que les changement que j'ai fait vont aider au moins \u00e0 la coh\u00e9rence du code et un peu pour les performances. Il semble que dans les conditions que j'ai test\u00e9 le nombre de tour soit plut\u00f4t fiable mais je pense que je devrai faire un peu de travail en aval dans la r\u00e9cup\u00e9ration de ces donn\u00e9es car je sens que cela va poser probl\u00e8me quelques fois. Je pense que en utilisant bien l'historique on peut potentiellement se passer de l'utilisation de ce chiffre pas toujours compl\u00eatement fiable. Mais sinon aujourd'hui c'est encore une fois un gros jour de doc. J'essaie d'expliquer les diff\u00e9rents proc\u00e9d\u00e9s avant de les oublier. J'essaie aussi de donner un maximum d'exemples sous formes de photos interm\u00e9diaires 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\u00e9diaires. En plus de la documentation je me suis aussi beaucoup occup\u00e9 de nettoyer mon code et je suis assez content par ce que m\u00eame en ayant du rajouter des couches de complexit\u00e9 pour mieux reconnaitres les temps au tour j'arrive \u00e0 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\u00e9 aujourd'hui et je sui bien mort. Faire autant de documentation et de nettoyage de code c'est pas forc\u00e9ment bon pour le cerveau je crois. J'ai besoin d'une sieste. Demain je pense que je vais commencer \u00e0 avancer sur la partie r\u00e9cup\u00e8ration des images. Je sais que la je fais un peu passer les tests \u00e0 la trappe mais d\u00e9ja j'en ai fait tout le long du d\u00e9veloppement de OCR_DECODE et il faut vraiment que j'avance, quitte \u00e0 revenir dessus quand j'aurai merge les deux projets ensemble. 26 Avril 2023 Aujourd'hui je vais devoir m'occuper de la partie r\u00e9cup\u00e9ration des images. J'ai d\u00e9ja eu l'occasion d'avancer sur ce projet pendant mopn poc et mes vacances. Donc la le but ca va \u00eatre de voir ce qui manque comme v\u00e9ritables features et ensuite je vais pouvoir m'occuper de la vue et de son int\u00e9gration avec le d\u00e9codage. Ok donc maintenant que j'au un programme qui arrive \u00e0 prendre des images depuis la F1TV correctement et en bonne r\u00e9solution. Je pense qu'il est temps de passer \u00e0 l'impl\u00e9mentation de la Forme que ca va prendre. C'est important de se poser au moins cinq minutes la question de comment je pr\u00e9vois de faire car m\u00eame si ca n'est pas la version finale, cette derni\u00e8re prendra tr\u00e8s fort inspiration du desing que je vais faire. Dans cette form j'aurais besoin de : Pouvoir selectionner un Grand Prix en ins\u00e8rant 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\u00e9ja un certain nombre de tours lanc\u00e9s. Et c'est \u00e0 peu pr\u00e8s tout en fait... J'ai tellement pouss\u00e9 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\u00e9menter la calibration qui elle aura besoin d'une UI un peu plus bal\u00e8ze. On pourrait m\u00eame imaginer que la calibration fasse partie int\u00e9grante des settings... Ca serait peut-\u00eatre bien que quand l'application se lance on se retrouve sur la page principale d'affichage de donn\u00e9es 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\u00e9velopp\u00e9e pour regrouper tout ca : \"Screen\" La police le style le placement et les couleurs ne sont pas d\u00e9finitfs, cependant je pense que c'est un bon d\u00e9but. Le but maintenant va \u00eatre de permettre de faire fonctionner la calibration et la r\u00e9cup\u00e8ration d'images. Si j'arrive \u00e0 faire fonctionner ces deux choses sur un m\u00eame projet avant la fin de la semaine cela serait super ! Bon J'ai pu avancer sur l'int\u00e8gration de Selenium mais cela prend un peu de temps car je veux impl\u00e9metner un moyen de pouvoir prendre une Screenshot \u00e0 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\u00e9rences qui obligent \u00e0 presque tout r\u00e9\u00e9crire. En fait le programme de calibration avait d\u00e9ja impl\u00e9ment\u00e9 la fonction de Windows et de Zones mais il fonctionnait juste assez diff\u00e9remment pour qu'il faille tout refaire. La je suis en train de perdre \u00e9norm\u00e9ment de temps \u00e0 cause d'un soucis de coordonn\u00e9es. J'ai repris le code de la calibration pour detecter ou l'utilisateur a cliqu\u00e9 pour cr\u00e9er les zones. Cependant, je n'arrive pas \u00e0 le faire fonctionner correctement. La zone est tout le temps d\u00e9cal\u00e9e en haut et en bas mais pas de la m\u00eame 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'\u00e9tait un simple d\u00e9calage cela ne serait pas compliqu\u00e9 \u00e0 g\u00e8rer mais la... J'ai un soucis \u00e9galement avec la r\u00e9solution des screenshots que je r\u00e9cup\u00e8re en full Headless. Voici un exemple de r\u00e9solution que j'arrive \u00e0 r\u00e9cup\u00e8rer sans le headless : \"High Res\" \"Low Res\" Il y a clairement un soucis et le probl\u00e8me c'est que avec une r\u00e9solution pareille, impossible de faire une reconnaissance correcte. BON J?EN PEUX PLUS LA. Ca fait des heures que je bosse sur ce probl\u00e8me d\u00e9bile et impossible de trouver une solution. J'ai essay\u00e9 cinq facons de forcer le browser headless a prendre une plus haute r\u00e9solution aucune ne fonctionne je ne comprends pas. A chaque fois que je me retrouve avec une r\u00e9solution de 1366 x 768 Ou une variante de basse r\u00e9solution du style. J'en peux plus je ne trouve aucune r\u00e9ponse sur internet ni m\u00eame 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\u00e9 est de changer ma version de GeckoDriver. Sauf que ca m'oblige \u00e0 changer les versions de mes libraries ce qui est tr\u00e8s p\u00e9nible, je vais continuer le debugging dans le projet Selenium_clean. Il faut savoir que la librairie de Selenium que j'utilise est bloqu\u00e9e en 0.27 ce qui fait que je ne peux utiliser qu'une version obsol\u00e8te du Gecko Driver. J'ai tent\u00e9 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\u00e9solution DPI, j'essaie de changer les param\u00eatres par d\u00e9faut des player de Firefox, j'essaie de changer la r\u00e9solution pendant et au d\u00e9but de l'execution IMPOSSIBLE DE FAIRE MARCHER CETTE MERDE C'EST PAS POSSIBLE !!! J'ai essay\u00e9 avec chrome mais je ne peux pas l'utiliser car les DRM m'emp\u00eacheront de prendre des screenshot du flux vid\u00e9o. J'ai essay\u00e9 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 \u00e9nerv\u00e9 ca se voit? A bon? Et maintenant NUGGET ne fonctionne plus... j'en peux plus la. Je ne peux plus t\u00e9l\u00e9charger de librairie sur aucun de mes projets... J'ai tent\u00e9 de supprimer le fichier de config et red\u00e9marrer Visual Studio mais cela ne fait rien. J'ai aussi tent\u00e9 de faire un 'nugget restore' toujours rien. Bon apparemment je ne suis pas le seul qui ne peut pas acc\u00e8der \u00e0 Nuget donc bon c'est pas juste chez moi qu'il y a un soucis. Mais m\u00eame en mettant ma 4G pour me connecter, je n'arrive pas \u00e0 acc\u00e8der \u00e0 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\u00e9marr\u00e9 trois fois mon pc et visual studio, j'ai essay\u00e9 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\u00e9seau soit en meilleur \u00e9tat. Lundi 1 Mai 2023 Bon je bosse depuis chez moi donc j'esp\u00e8re que Nuget va mieux fonctionner. Apr\u00e8s un weekend \u00e0 r\u00e9fl\u00e9chir 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\u00e9solution de 1920x1200 comme moi. Cela veut donc s\u00fbrement dire que le soucis vient de cette r\u00e9solution de laptop comme moi. Si vraiment je n'arrive pas dans un premier temps \u00e0 faire fonctionner le Headless correctement, je peux toujours laisser la page de c\u00f4t\u00e9 et m'occuper du reste du programme. Certes ca serait vraiment infernal d'avoir \u00e0 garder une page chrome ouvert en tous temps et en plus elle doit \u00eatre en plein \u00e9cran mais bon... Si il n'y a vraiment pas d'autres solutions malheureusement je serai bien oblig\u00e9. BON ! JE N'ARRIVE MEME PLUS A FAIRE UN PROJET QUI UTILISE SELENIUM ET QUI MARCHE JE VAIS FAIRE BR\u00dbLER 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\u00eame putain de librairies que sur les autres projets les m\u00eames versions, je prend le m\u00eame exact code. Sur le nouveau projet impossible de le faire fonctionner. Je commence \u00e0 croire que on essaie de me faire p\u00eater un cable. Du coup dans un \u00e9lan de d\u00e9sespoir 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 \u00e0 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\u00eame 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\u00f4t partir sur la librairie GeckoFX qui semble pouvoir contr\u00f4ler une instance de firefox. Mais j'avais justement pris un putain de projet C# et pas JS pour ne pas me taper ces probl\u00e8mes de librairies... Et si cette option ne fonctionne pas mon dernier espoir sera de directement int\u00e9ragir avec le geckodriver.exe et la ca risque de pas \u00eatre dr\u00f4le. 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 \u00e0 rien je commence \u00e0 devenir fou. Tout ce travail pour rien c'est pas possible. M\u00eame en essayant directement d'int\u00e9ragir avec le process geckodriver.exe je ne peux pas arriver \u00e0 mes fins. J'arrive \u00e0 lancer le service et tout, mais je n'arrive pas \u00e0 vraiment contr\u00f4ler ce qu'il se passe donc impossible de venir prendre des screenshot. Je ne sais tout simplement pas quoi faire ... Je suis bloqu\u00e9. Je me suis cass\u00e9 la t\u00eate \u00e0 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 \u00eatre dr\u00f4le. Ok alors j'ai tout repris depuis le d\u00e9but et je crois que j'ai enfin une solution. Pour la trouver j'ai re-essay\u00e9 toutes les techniques que j'avais tent\u00e9 avant mais dans l'ordre et en les isolant \u00e0 chaque fois. Cela inclus : Tenter de changer la densit\u00e9 de pixels. En effet je me suis dit que comme la r\u00e9solution \u00e9tait plus basse le soucis \u00e9tait que le virtual screen avait simplement une DPI r\u00e9duite. profile.SetPreference(\"layout.css.devPixelsPerPx\", \"2.0\"); J'ai aussi tent\u00e9 de r\u00e9duire \u00e0 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\u00e9 tout b\u00eatement de rajouter dans la liste des arguments la taille voulue de l'\u00e9cran. 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\u00eatre \u00e0 \u00eatre plus grande. js.ExecuteScript(\"window.resizeTo(1920, 1080);\"); Comme cela n'a pas march\u00e9 j'ai pu lire que cela pouvait \u00eatre la taille int\u00e9rieure qui devait \u00eatre chang\u00e9e js.ExecuteScript(\"window.innerWidth = 1920; window.innerHeight = 1080;\"); Encore une fois sans succ\u00e8s. J'ai ensuite tent\u00e9 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\u00e9 quoi que ce soit \u00e9tait 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\u00eame temps, qui elle ne marchait pas mais qui emp\u00eachait 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'\u00e9tait pas normal alors je me disais que cette technique ne marchait pas non plus et je l'ai pass\u00e9e. Alors tout n'est pas encore gagn\u00e9, il faut que j'arrive \u00e0 impl\u00e9menter ca dans un plus gros projet et que la vid\u00e9o puisse \u00eatre 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\u00e8me de resize, maintenant la page fait 100x100 et est grise. Il doit y avoir une technique que j'ai oubli\u00e9 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\u00e9l\u00e9charger EXACTEMENT les m\u00eames librairies que sur mon autre projet et j'utilise l'EXACT m\u00eame geckodriver.exe mais dans le projet principal impossible de lui faire chier une image m\u00eame avec l'EXACT m\u00eame 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\u00e8s avoir supprim\u00e9 l'int\u00e9gralit\u00e9 de ma classe Emulator cela semble marcher un peu mieux. Je ne vais pas m'\u00e9tendre sur la castrophe niveau temps que cela repr\u00e9sente. Si au moins j'arrive \u00e0 faire fonctionner quelque chose je suis content. Maintenant j'ai un soucis un peu sp\u00e9cial. Depuis que j'ai chang\u00e9 la r\u00e9solution, il semble que le programme aie du mal \u00e0 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 \u00e0 trouver le bouton : \"ERROR 105\" \"ERROR 105\" 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 \u00e9l\u00e9ment 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\u00e8me vienne du GP d'azerbidjan. En effet, quand je teste un autre Grand Prix tout va bien. ET MERDE ! J'ai r\u00e9ussi \u00e0 avoir des images en 1080P mais d\u00e9s que je passe l'image en plein \u00e9cran c'est de nouveau du 1366X768 Avant de mettre en plein \u00e9cran: \"Before fullscreen\" Apr\u00e8s: \"After fullscreen\" On peut voir sur l'image que l'option 1080P est effectivement bien selectionn\u00e9e mais il doit y avoir un param\u00e8tre de Firefox qui s'occupe de la r\u00e9solution d'un player vid\u00e9o. Il va juste falloir trouver ce param\u00eatre... J'ai essay\u00e9 d'utiliser : Driver.Manage().Window.Size = new System.Drawing.Size(windowWidth, windowHeight); Sans succ\u00e8s. 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\u00e9 la solution en trouvant cette commande profile.SetPreference(\"full-screen-api.enabled\", true); Mais non toujours pas... Je commence \u00e0 perdre patience. C'EST BON. Apr\u00e8s litt\u00e9rallement 3h de debugging avec M.Bonvin (Que je remercie IMMENSEMENT) on a r\u00e9ussi \u00e0 trouver au fin fond d'un thread github que la valeur \u00e9tait hard cod\u00e9e 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\u00e8gler a \u00e9t\u00e9 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 \u00e0 Payerne Mercredi et Jeudi. Si vous \u00eates curieux je peux vous dire que comme il n'y avait presque plus de places cet \u00e9t\u00e9 je ferai Canonnier Lance mines. C'\u00e9tait assez frustrant d'avoir perdu deux jours de travail mais on va faire avec. Vendredi 5 Mai 2023 Bon malgr\u00e9s les courbatures il faut que je me mette au boulot un peu serieusement par ce que sinon ca va \u00eatre compliqu\u00e9 de rattraper mon retard. La derni\u00e8re fois si je me souviens bien j'avais r\u00e9ussi \u00e0 trouver un moyen de prendres des images en bonne r\u00e9solution. Il faut maintenant que je commence \u00e0 faire fonctionner la calibration et ce qui serait bien ca serait que je commence \u00e0 ajouter la partie OCR au projet. Il faut que je me d\u00e9p\u00eache 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'\u00e9tait pas la bonne, l'image \u00e9tait recentr\u00e9e ce qui fait que cela faussait totalement les r\u00e9sultats. Quand on fait en sorte que l'image prenne toute la place, les coordonn\u00e9es sont prises correctement. Voici un exemple d'ou en est la partie calibration. \"Exemple settings UI\" Normalement il me suffit d'impl\u00e9menter les windows, et on devrait relativement facilement ajouter les pilotes. Et voila. J'ai pu impl\u00e9menter les windows et les pilotes. Et je peux aussi exporter des presets et les loader. Bon le loading est un peu beugg\u00e9 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 \u00eatre l'impl\u00e9mentation de l'OCR. Lundi 8 Mai 2023 Aujourd'hui c'est journ\u00e9e Poster. Je pense que je ne vais pas finir la journ\u00e9e content car les limitations sont un peu trop pr\u00e9sentes. J'ai fait une version que Garcia pourrait accepter, c'est \u00e0 dire en noir et blanc et avec un tout petit peu plus de d\u00e9tail. \"Poster V3\" 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-\u00eatre pas la meilleure id\u00e9e. Je me suis dit que la bonne id\u00e9e serait peut-\u00eatre de prendre un autre circuit pour qu'il y aie bien trois parties : \"Poster V4\" 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. \"Poster V5\" Je me suis ensuite dit que le circuit n'\u00e9tait peut \u00eatre tout simplement pas une bonne id\u00e9e. J'ai donc essay\u00e9 de faire quelque chose de plus classique avec juste un peu de background pour qu'on puisse \u00e9viter le soucis de la page blanche derri\u00e8re : \"Poster V6\" Puis je me suis dit que finalement le circuit me manquait. Alors j'ai d\u00e9cid\u00e9 de combiner le background et le circuit ainsi que simplifier l\u00e9g\u00e8rement les diagrammes en retouchant un peu tout le reste on pouvait arriver \u00e0 quelque chose de sympatique : \"Poster V7\" Je ne suis pas content \u00e0 100% mais bon je pense que je vais m'en satisfaire. Pour donner une id\u00e9e de la gal\u00e8re que c'est de cr\u00e9er un poster, voici ce \u00e0 quoi ressemble mon espace de travail Figma : \"Bordel 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\u00e9cup\u00e8ration d'images. En effet, je pense que je n'aurai plus besoin de changer grand chose \u00e0 ce niveau. Mais je ne ferai pas la partie analyse fonctionnelle car l'interface n'est clairement pas termin\u00e9e. En fait j'avais oubli\u00e9 mais j'ai eu un rendez vous m\u00e9dical 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 \u00e0 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 \u00e0 l'arm\u00e9e m'ont fait perdre un peu la notion du temps car j'avais l'impression que l'evaluation interm\u00e9diaire 1 \u00e9tait 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\u00e9cup\u00e8ration d'images. Je pense que je vais aussi ajouter la partie calibration \u00e0 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\u00e9 depuis la derni\u00e8re fois. Et puis je pense que la partie calibration et r\u00e9cup\u00e8ration 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\u00e9ja faite pendant les premiers jours alors ca devrait aller. Pour le rendu il nous \u00e9tait demand\u00e9 de fournir un fichier PDF avec tout dedans avec une table des mati\u00e8res 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: \u00a9CFPTI 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\u00e9 pour cette partie et avoir cr\u00e9\u00e9 un plugin qui me permet de mettre mon code source directement dans le pdf. Bon au final j'ai quand m\u00eame chang\u00e9 mon poster \"Poster V8\" Mais je suis trop attach\u00e9 \u00e0 l'ancien concept alors je vais plut\u00f4t utiliser ca : \"Poster V9\"","title":"Journal de bord"},{"location":"jdb.html#journal-de-bord","text":"","title":"Journal de bord"},{"location":"jdb.html#mercredi-29-mars-2023","text":"Premier jour du travail de dipl\u00f4me. Nous avons eu un briefing de mr Garcia et nous avons pu commencer \u00e0 pr\u00e9parer le travail. Nous avons eu les diff\u00e9rents fichiers nescessaires \u00e0 la bonne r\u00e9alisation du projet et je me suis mis \u00e0 faire les fichiers nescessaires La premi\u00e8re chose a \u00e9t\u00e9 de faire ce mkdocs dans lequel j'ai mis un fichier yml plut\u00f4t standart qui risque de changer au fur et \u00e0 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 \u00e0 quoi ca ressemble en forme de site \"Exemple mkdocs\" Ensuite il m'a fallu faire une version plus \u00e0 jour de mon cahier des charges car je n'y avait pas touch\u00e9 depuis novembre. J'ai envoy\u00e9 un mail \u00e0 mes enseignants pour qu'ils puissent y jeter un oeuil pour \u00eatre s\u00fbr que je n'ai rien chang\u00e9 qui les d\u00e9rangent. Monsieur Jayr m'a demad\u00e9 \u00e0 l'occasion de lui faire un planning type Gantt alors je me suis mis \u00e0 la t\u00e2che. J'ai fait un planning pr\u00e9visionnel et une l\u00e9gende les deux sont dispo dans le dossier planning de ce repertoire. Ensuite je me suis mis \u00e0 tout mettre sur git. A commencer par ce repertoire Et c'est deja la fin de la journ\u00e9e ! Demain j'avance un peu sur la doc avec ce que je peux d\u00e9ja remplir et je finis de pr\u00e9parer ce dont j'ai besoin pour commencer \u00e0 coder.","title":"Mercredi 29 Mars 2023"},{"location":"jdb.html#jeudi-30-mars-2023","text":"Aujourd'hui selon le planning je dois me charger des dernirers pr\u00e9paratifs pour commencer correctement. J'ai fait expr\u00e8s de prenre du temps pour ca au d\u00e9but pour ne pas me cr\u00e9er de soucis plus loin pendant le travail. Je vais envoyer par mail le planning que j'ai fait \u00e0 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 \u00e0 modifier quelques aspects au fur et \u00e0 mesure. J'ai aussi d\u00e9sactiv\u00e9 mkdocs with pdf par ce que les r\u00e9sultats ne sont vraiment pas ceux que j'attends et cela ralentis \u00e9norm\u00e9ment le d\u00e9ploiment. J'ai aussi rassembl\u00e9 mes croquis pour le poster : \"Croquis Poster 1\" \"Croquis Poster 2\" On peut voir que dans un premier temps j'ai tent\u00e9 de faire un poster un peu plus stylis\u00e9 et marketing. Cependant apr\u00e8s avoir discut\u00e9 avec Mr Garcia et diff\u00e9rents profs dont un de l'HEPIA et ils m'ont indiqu\u00e9 que ce qui \u00e9tait attendu \u00e9tait moins du marketing qu'un diagramme de fonctionnement. On peut voir sur les derniers posters que le cot\u00e9 technique ressort de plus en plus. Le but sera de faire une version encore plus technique ou on peut voir les diff\u00e9rents fonctionnements de l'application avec les technologies utilis\u00e9es. Le d\u00e9fi cela va \u00eatre de faire un joli poster qui soit en m\u00eame temps vendeur et en m\u00eame temps rempli techniquement. Oh et j'ai eu un probl\u00e8me ou mon calvier et ma souris ne voulaient d'un coup plus fonctionner. Dans mon cas c'\u00e9tait un probl\u00e8me de power management des ports. J'ai eu le soucis sur mon pc fixe \u00e0 la maison et sur mon pc portable \u00e9galement. En gros de ce que j'ai compris le soucis c'est que le pc croit que un port est trop solicit\u00e9 niveau puissance et du coup d\u00e9cide de couper l'alimentation du port USB. J'ai pu r\u00e8gler le soucis en allant dans le device manager sous universal bus controller sous power management et en d\u00e9cochant la case qui indique que windows peut d\u00e9sactiver ce port. Je ne conseille pas ce fix si vous avez des composants de mauvaise qualit\u00e9 car cela pourrait \u00eatre une vraie alerte cependant le fait que mes composants sont plut\u00f4t haut de gamme et le fait que mon clavier et ma souris le fassent en m\u00eame temps et que ils fonctionnaient tr\u00e8s bien depuis plus de 4 ans me font penser que c'est juste une nouvelle mise a jour de windows qui est p\u00e9nible. Demain je vais pouvoir commencer \u00e0 coder pour de bon.","title":"Jeudi 30 Mars 2023"},{"location":"jdb.html#vendredi-31032023","text":"Aujourd'hui on s'occupe de la PT2 qui est la programmation de la r\u00e9cup\u00e8ration 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 \u00e0 la reconnaissance. Je peux soit utiliser l'image crop\u00e9e directement : \"Image non trait\u00e9e\" Soit avec un filtre pour passer en noir et blanc laxiste \"Image trait\u00e9e laxiste\" Soit avec un filtre pour passer en noir et blanc stricte \"Image trait\u00e9e stricte\" Il va falloir faire des tests avec tous les noms et les chiffres pour trouver le plus efficace. Bon malheureusment Iron OCR semblait \u00eatre une bonne alternative mais c'est une librairie priv\u00e9e qui demande une license pour \u00eatre utilis\u00e9e. 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\u00f4t 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 \u00e9taient autrefois mal reconnus sont parfaitement interpr\u00eat\u00e9s. Par exemple voici un exemple de reconnaisance de texte sur tous les pilotes : \"Screenshot de reconnaisance d'image complete\" On voit que le nom Leclerc est mal reconnu. Mais voici ce que cela donne quand on prend une image qui ne contient que le nom Leclerc : \"Screenshot de reconnaissance d'image crop\u00e9e\" On voit ici que le nom Leclerc est tr\u00e8s bien reconnu. Dans le premier exemple on peut voir que Tsunoda est reconnu comme \"Reticin\" ce qui n'est pas exactement pareil (lol) Et quand on isole le nom Tsunoda dans une image seule : \"Screenshot de reconnaissance de Tsunoda\" Il le lit \"RETLELYY\" ce qui n'est toujours pas exactement ca... Une meilleure r\u00e9solution pourrait peut-\u00eatre r\u00e9soudre le probl\u00e8me en partie. Jusqu'ici les images \u00e9taient en presque 720P ce qui donne ceci : \"Tsunoda en 720P\" Et j'ai lanc\u00e9 une r\u00e9cup\u00e8ration d'images en 1080p pour r\u00e9cup\u00e8rer ceci : \"Tsunoda en 1080P\" On peut voir une certaine diff\u00e9rence tout de m\u00eame. Et quand on lance la reconnaissance : \"Reconnaissance Tsunoda en 1080P\" \"Tsunoda n'est plus \u00e9crit \"RETLELYY\" mais \"TSUNDDA\" ce qui n'est pas parfait mais qui est d\u00e9ja beaucoup mieux. J'ai essay\u00e9 de mettre l'engine de Tesseract en mode \"JPN\" comme Tsunoda est un nom japonais mais sans succ\u00e8s j'ai le m\u00eame r\u00e9sultat. Comme la r\u00e9solution est meilleure je me suis dit que peut \u00eatre le filtre de passage en noir et blanc pourrait aider. J'ai \u00e9crit 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\u00e9 avec un treshold de 100 et le programme a r\u00e9ussi \u00e0 me sortir Tsunoda en deux mots ce qui \u00e9tait d\u00e9ja tr\u00e8s encourageant. Et apr\u00e8s avoir augment\u00e9 le Treshold... Tada : \"Tsunoda 1080P avec filtre\" Le programme arrive bien \u00e0 reconnaitre TSUNODA. Je pense que cette tactique ne fonctionnait pas avant car la resolution \u00e9tait trop faible et l'aliasing se m\u00ealait trop avec le texte pour \u00eatre utilisable. Cependant cette technique ne fonctionne pas sur tous les noms. Par example avec Leclerc : \"Leclerc 1080P avec filtre\" On r\u00e9cup\u00e8re \"Leeler'c\" ce qui n'est pas bon du tout. Mais en modulant le Treshold (ici \u00e0 150) On peut de nouveau voir Leclerc \u00eatre reconnu correctement \"Leclerc 1080P avec filtre 2\" Je pense que pour avoir de bons r\u00e9sultats il va falloir faire un algo qui : D\u00e9coupe l'image en autant de plus petites images pour avoir un mot par image. Teste voir si avec l'image originale un nom correspond \u00e0 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\u00e9s. Seulement voila, il n'y a pas que des lettres que l'on veut r\u00e9cup\u00e8rer. On veut surtout pouvoir r\u00e9cup\u00e8rer les chiffres. Pour les chiffres on va avoir des soucis \u00e9galement... Si on essaie directement la m\u00eame technique sans filtre on a des r\u00e9sultats comme celui ci : \"Tentative de reconnaisance du timing\" La virgule a tendeance \u00e0 se barrer ce qui est particuli\u00e8rement probl\u00e9matique. Cependant comme les chiffres ont beaucoup moins de possibilit\u00e9es que les lettres et qu'il n'y a pas de probl\u00e8me de langue on devrait pouvoir travailler \u00e0 faire des r\u00e8glage que l'on pourra ensuite utiliser. Avec un Treshold de 165 on arrive presque \u00e0 quelque chose d'int\u00e9ressant : \"Tentative 2 de reconnaissance du timing\" Le + n'est clairement pas compris mais ca n'est pas emb\u00eatant car c'est souvent redondant. On arrive cependant \u00e0 isoler 3 et 259. M\u00eame si la virgule n'est pas comprise cela veut dire qu'il est tout de m\u00eame possible de discriminer les secondes des milisecondes. Maintenant avec un temps au tour : \"Reconnaissance du timing au tour\" On arrive sans rien changer aux param\u00eatres \u00e0 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 \u00e0 plus grande \u00e9chelle 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 \u00e0 quel point je peux faire confiance \u00e0 la reconnaissance des chiffres. Automatiser un syst\u00e8me de test de la sorte me sera tr\u00e8s utile dans le futur pour v\u00e9rifier 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\u00e9brouille avec les nombres de tours qui se trouvent dans les icones de pneus.","title":"Vendredi 31/03/2023"},{"location":"jdb.html#lundi-3-avril","text":"Aujourd'hui on va faire un programme qui permet de cr\u00e9er un dataset qui permette de tester le programme de reconnaissance. Je pense que le meilleur moyen de faire serait un programme qui cr\u00e9e le dataset et qui ensuite peut tester diff\u00e9rentes methodes de reconnaissance. Par la m\u00eame occasion je peux d\u00e9velopper la technologie qui va permettre de d\u00e9couper 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\u00e9ja avoir une id\u00e9e de la structure de mon programme. Pour le moment je r\u00e9flechis \u00e0 un syst\u00e8me de \"Zones\" et de \"Windows\". L'id\u00e9e serait que une Zone est juste une sous partie d'image qui peut encore \u00eatre d\u00e9compos\u00e9 tandis que chaque Window contient une ou plusieurs informations \u00e0 r\u00e9cup\u00e8rer. J'ai essay\u00e9 de d\u00e9couper l'image pour que cela soit plus clair : \"Main zone\" Ici on peut voir que l'image est d\u00e9coup\u00e9e en plusieurs grandes zones. Dans un premier temps on ne s'occupe que de la premi\u00e8re. Ensuite : \"Driver zone #1\" On peut voir la que cette Main zone serait elle m\u00eame d\u00e9compos\u00e9e en plusieurs plus petites zones. Et ensuite chacunes de ces petites zones : \"Driver windows\" Sera d\u00e9compos\u00e9e 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\u00e9flechissant on pourrait tout \u00e0 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\u00e9e de : Une image de d\u00e9part Un rectangle qui la positionne sur cette derni\u00e8re Une liste de zones (potentiellement vide) Une liste de windows (potentiellement vide) Une methode qui permet de r\u00e9cup\u00e8rer une image de la zone Une methode qui permet de lancer la reconnaissance sur chaque window Une window serait compos\u00e9e de : Une image de d\u00e9part (cela peut \u00eatre l'image crop\u00e9e de la zone parente peu importe) Un rectangle qui la positionne sur cette derni\u00e8re Une methode qui permet de r\u00e9cup\u00e9rer un image de la window Une methode qui permet de lancer la reconnaisance sur l'image (Chaque type de zone doit l'impl\u00e9menter) 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\u00e9cup\u00e8rer dans une base de donn\u00e9e ou un objet. Par exemple une Zone de pilote pourrait tr\u00e8s bien contenir un objet pilote et le donner \u00e0 ses windows qui rempliraient ce m\u00eame objet. C'est une reflexion plus stockage que OCR mais c'est int\u00e9ressant pour savoir ce que fait une window des donn\u00e9es qu'elle r\u00e9cup\u00e8re. Dans un premier temps je pense que les windows vont simplement \u00e9crire dans un fichier ce qu'elles trouvent chacunes dans le format qu'elles veulent. Pour comprendre pourquoi je me prend la t\u00eate il faut savoir que chaque window peut avoir acc\u00e8s \u00e0 pleins d'informations diff\u00e9rentes. 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\u00e9ment pour un type de pneu ou un DRS ouver. Comme chaque window a plusieurs types de data elle devra elle m\u00eame se charger de comment la traiter ET de la stocker. Voila un diagramme qui r\u00e9sume comment je vois l'impl\u00e9mentation dans un premier temps : \"Diagramme d'explications\" Voici comment se pr\u00e9sente 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\u00e9er diff\u00e9rent types de Zones. Par exemple la MainZone devra d\u00e9couper son contenu en 20 parties \u00e9gales pour tenter de chopper les 20 pilotes. Il serait cool de trouver un moyen de calibrer automatiquement. C'est peut-\u00eatre 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-\u00eatre determiner des lignes. Et voici le squelette d'une window g\u00e9n\u00e9rique 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\u00eame impl\u00e9menter la r\u00e9cup\u00e8ration 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\u00e9rification 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\u00e9er une Main window qui se calibre toute seule. Alors apr\u00e8s avoir bien gal\u00e8r\u00e9 avec l'interface pour permettre au user de cliquer sur la form pour voir les zones qu'il cr\u00e9e, j'ai pu cr\u00e9er un zone qui fait les dimensions de MainZone et j'ai pu lancer la reconnaissance sur l'image et voir ou il trouve du texte : \"MainZone avec carr\u00e9s de texte\" Maintenant il faut que je nettoie la liste de rectangle pour exclure ceux qui sont trop grands pour \u00eatre sur une seule ligne, ceux qui indiquent le nombre de tour en haut et ceux qui n'ont pas d'int\u00e9r\u00eats. On pourra ensuite isoler les lignes et cr\u00e9er 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 \u00e0 l'utilisateur de ne pas les prendre dans la screenshot. Comme ils contiennent des mots qui peuvent \u00eatre utilis\u00e9s plus loin dans les data je ne peux pas les blacklister et faire un syst\u00e8me qui s'occupe de les enlever si ils existent selon le position y me prendrait trop de temps pour rien. Apr\u00e8s avoir filtr\u00e9 un peu les resultats et enlev\u00e9 les zones beaucoup trop grandes, on se retrouve d\u00e9ja plus qu'avec ca : \"MainZone avec de meilleurs carr\u00e9s\" Comme on peut le voir, du c\u00f4t\u00e9 gauche de l'image on a beaucoup de choses reconnues mais avec beaucoup de tailles diff\u00e9rentes ce qui n'est pas id\u00e9al. Alors j'ajoute un filtre qui permet de ne selectionner que les data sur la droite. \"MainZone avec de meilleurs carr\u00e9s\" Maintenant il devrait \u00eatre possible de faire un algorythme qui ne prend que un seul carr\u00e9 par ligne. \"MainZone avec un seul carr\u00e9 par ligne\" Maintenant que on sait ou se trouve chaque ligne on peut faire un petit traitement et d\u00e9couper l'image en plusieurs windows. Et voila : \"Mainzone auto calibr\u00e9e\" Maintenant le programme peut cr\u00e9er des zones pour chaque pilote \"Images pilotes\" \"Zone d'un pilote\" Maintenant il faut que j'impl\u00e9mente un syst\u00e8me un peu similaire pour cr\u00e9er des windows. Voici la methode que j'ai cr\u00e9\u00e9 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 \u00e9norme comme code mais pour tout mettre en place ca demande quand m\u00eame 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\u00e9 d'ajouter des windows sur une zone. J'ai juste quelques difficult\u00e9es \u00e0 les ajouter correctement, j'ai un offset tout pourri qui se met tout le temps \"Sainz coup\u00e9\" \"Perez coup\u00e9\" Cela doit \u00eatre un soucis lors de la detection de clic qui met un offset en trop. C'est vraiment p\u00e9nible 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 \u00eatre sauvegard\u00e9e comme ca pas besoin d'\u00e0 chaques fois le refaire. C'est bon ! J'avais juste oubli\u00e9 de changer le calcul d'offset entre le code de la zone et de la window. Note pour plus tard, il serait peut-\u00eatre judicieux de faire quelque chose pour la vue, les windows et les Zones ont le m\u00eame exact comportement pour la vue ce qui fait dupliquer du code. Mais au moins maintenant ca fonctionne : \"Ocr tester screenshot\" Et le programme va directement cr\u00e9er un dossier par pilote avec toutes les images de chaque Data le concernant : \"Dossier Perez\" ; Et c'est tout pour aujourd'hui je pense. Ce qui serait cool demain c'est que je puisse stocker d'une mani\u00e8re ou d'une autre ces fichiers de calibration et que je puisse les transf\u00e8rer vers le programme qui va s'occuper de d\u00e9coder et commencer gentillement \u00e0 d\u00e9coder les diff\u00e9rents types de data. Note pour quand je ferai les tests. Je pense que la meilleure id\u00e9e 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\u00e9codage. Pour le moment on est plut\u00f4t dans les clouts niveau planning.","title":"Lundi 3 Avril"},{"location":"jdb.html#mardi-4-avril","text":"Aujourd'hui je suis scens\u00e9 plut\u00f4t bosser sur l'interpretation des donn\u00e9es, mais une id\u00e9e m'a taraud\u00e9 l'esprit toute la nuit. Est-ce que je ne pourrais pas quand m\u00eame essayer de d\u00e9composer la zone de pilote directment comme pour la Main zone. Pour ce faire j'ai tent\u00e9 de faire comme pour la main zone c'est \u00e0 dire lancer la reconnaissance pour savoir ou \u00e9taient tous les champs de donn\u00e9es mais malheureusement je ne pense pas que cela va \u00eatre possible. En effet non seulement ici les champs sont de tailles tr\u00e8s vari\u00e9es, mais en plus la reconnaissance n'arrive pas \u00e0 en r\u00e9cup\u00e8rer le m\u00eame nombre sur chaque ligne ce qui risque d'\u00eatre complexe \u00e0 utiliser ensuite. La preuve : \"Tentative d'auto calibration\" ; Cependant tout n'est pas perdu ! Il y a peut-\u00eatre 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-\u00eatre hard coder la valeur des diviseurs et les utiliser pour cr\u00e9er des zones. Ok alors visiblement c'est un probl\u00e8me car il semble y avoir d'autres pixels de cette couleur dans l'image (Qui l'aurait cru lol) \"Tentative 2\" J'a tent\u00e9 de r\u00e9duire la tol\u00e9rance mais le soucis c'est que c'est soit trop soit pas assez Derni\u00e8re tentative, j'ai essay\u00e9 de prendre plusieurs pixels en hauteur pour chaque incr\u00e9ment de X et en faire la moyenne, et m\u00eame comme ca, impossible de trouver de mani\u00e8re efficace les zones. Je pense que je vais donc revert tous mes changements pour revenir \u00e0 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\u00e9 une feature importante. Les zones font la largeur indiqu\u00e9e par l'utilisateur mais elles font la hauteur max comme ca toutes les window font la m\u00eame hauteur et ca permet \u00e0 l'utilisateur de ne pas forc\u00e9ment \u00eatre ultra pr\u00e9cis dans sa selection. Ce qui nous donne : \"Resultat final\" Maintenant je dirais que les deux prochaines choses \u00e0 faire seraient de stocker ces zones dans un fichier JSON ou autre pour que la calibration puisse \u00eatre envoy\u00e9e 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'\u00e9crant 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\u00e9 plutot que d'\u00e9crire les coordonn\u00e9es en dur pour chacunes. Chaque Grande zone va impl\u00e9menter 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\u00e9sultat auquel j'aimerais arriver. Mais pour y arriver il faut encore que je cr\u00e9e les diff\u00e9rents types de window. Cela veut dire que je dois d\u00e9cider quelles informations je vais r\u00e9cup\u00e8rer de la page. Par exemple je vais conserver la position du pilote mais au final les changements de positions sont difficiles \u00e0 lire et sont redondants. Si je garde un historique des positions des pilotes je peux calculer moi m\u00eame les changements. Pareil pour gap avec la voiture devant. Je pense que je vais juste garder l'information des \u00e9carts absolus et ensuite je pourrai toujours calculer la diff\u00e9rence entre les pilotes. Ca peut para\u00eetre b\u00eate car cela rajoute du calcul mais en r\u00e9alit\u00e9 le calcul de l'OCR est extr\u00eamement gourmand alors il faut que j'\u00e9vite le plus possible d'y faire recours. Il est bien plus rapide de calculer les \u00e9carts que d'essayer de reconnaitre le texte et le convertir en chiffre. J'ai visiblement ajout\u00e9 un bug dans mon code. Maintenant tous les pilotes ont la m\u00eame image quand on les selectionne. Mais visiblement ca n'\u00e9tait pas le cas avant car j'avais pu prendres des images de chaque pilote. J'ai pass\u00e9 3 minutes \u00e0 fixer un bug stupide j'ai un peu envie de br\u00fbler ma place de travail... Mais bon au moins maintenant cela fonctionne ! Toutes les images sont r\u00e9cup\u00e8r\u00e9es et ont un format correct avec le bon nom : \"Verstappen folder\" Avec un peu de code tr\u00e8s moche j'ai pu cr\u00e9er un fichier JSON qui contient les diff\u00e9rentes 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 \u00eatre possible de faire un fichier qui ne contient que les infos d'un seul pilote car ensuite il y a simplement un offset \u00e0 appliquer sur la zone et les windows. Je vais donc pouvoir commencer enfin le logiciel de d\u00e9codage qui prend en entr\u00e9e un fichier JSON comme celui ci qui a \u00e9t\u00e9 g\u00e9n\u00e8r\u00e9 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\u00e9rents pilotes pr\u00e9sents sur le Grand Prix et ce genre d'infos. Quoique je vais l'ajouter d\u00e9ja maintenant et plus tard je mettrai en place la feature acessible depuis l'interface. Mais le hardcoder maintenant me permet d\u00e9ja de mieux coder l'autre c\u00f4t\u00e9. Ce programme n'est en aucun cas termin\u00e9 et je vais devoir travailler encore un peu dessus pour qu'il soit utilisable correctement mais au moins il fonctionne \u00e0 peu pr\u00e8s. 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\u00e9codage. Demain je dois finir le d\u00e9codage du JSON et je dois commencer \u00e0 impl\u00e9menter la reconnaissance des textes. Voire m\u00eame des pneus etc si j'y arrive.","title":"Mardi 4 Avril"},{"location":"jdb.html#mercredi-5-avril","text":"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 \u00e0 faire le programme de calibration mais je pense que c'est essentiel de prendre ce temps maintenant. (BTW il faudra quand m\u00eame retourner faire une plus jolie version par ce que la ca marche mais c'est tout) Bon apr\u00e8s pas mal de boulot je pense avoir r\u00e9ussi. Dans le nouveau programme on arrive \u00e0 r\u00e9cup\u00e8rer les diff\u00e9rentes zones : \"JSON decode result\" Un conseil de notre professeur M.Bonvin a \u00e9t\u00e9 de cr\u00e9er des Releases de versions qui ne fonctionnent pas ou pas tr\u00e8s bien. J'ai donc publi\u00e9 une premi\u00e8re release de l'OCR_TEST qui fonctionne vite fait. J'ai seulement un petit soucis, comme je recr\u00e9e compl\u00eatement la structure des driver zones avec seulement la premi\u00e8re, il y a un petit d\u00e9calage car entre les zones il y avait un gap. Ce qui fait que si la premi\u00e8re zone est parfaitement centr\u00e9e : \"Centered driver\" La vingti\u00e8me ne l'est plus exactement : \"Uncentered Driver\" Pour ca j'ai essay\u00e9 de mettre un espacement arbitraire mais c'est complexe. Je vais plut\u00f4t tenter de faire une diff\u00e9rence entre la taille de la zone compl\u00eate et de la taille additionn\u00e9e de toutes les fen\u00eatre et diviser le resultat entre toutes les fen\u00eatres. Ca n'est pas parfait mais au moins maintenant les donn\u00e9es ne touchent plus les bords de la fen\u00eatre. Et voila ! Maintenant avec le fichier de configuration en Json on arrive \u00e0 r\u00e9cup\u00e8rer toutes les infos comme si elles avaient \u00e9t\u00e9 envoy\u00e9es directement depuis l'app de calibration mais sans le processing time ! \"Verstappen folder 2 On peut donc ENFIN passer au d\u00e9codage de ces FICHUES donn\u00e9es. Je vais pouvoir impl\u00e9menter ce que j'ai fait dans le projet de test de d\u00e9codage. Gr\u00e2ce \u00e0 mon d\u00e9coupage initial qui m'a pris du temps \u00e0 impl\u00e9menter on a enfin un truc qui marche m\u00eame si je n'ai impl\u00e9ment\u00e9 que la reconnaissance de noms. \"Image reconnaissance propre\" Si on se rappelle du syst\u00e8me 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\u00fbrement un grand dataset qui prend des pilotes de reserves et des pilotes juniors pour que dans le cas ou un pilote est remplac\u00e9 dans l'ann\u00e9e il n'y a pas besoin de tout recalibrer avec l'application. Apr\u00e8s une discussion avec M.Bonvin j'ai d\u00e9cid\u00e9 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\u00e9rence entre deux strings. Bon et \u00e9videmment 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 \u00e0 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\u00eame pas id\u00e9al. Il va falloir que je trouve un moyen de le repond\u00e9rer. C'est dommage par ce que cela marche super avec Alonso Verstappen et Albon. J'ai un peu modifi\u00e9 la methode et j'ai fait en sorte d'envoyer tous les noms en majuscules en me disant que cela pourrait r\u00e9duire le nombre de changements. Et ca a march\u00e9 !! Cela va s\u00fbrement demander plus de tests pour \u00eatre bien s\u00fbr 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\u00e9ja fait une partie du boulot le premier jour alors je vais juste reprendre \u00e0 partir de l\u00e0. Je r\u00e9cup\u00e8re 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 \u00eatre trop dur \u00e0 g\u00e8rer. 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\u00e8s un peu de temps pour nettoyer la string etc on se retrouve avec un r\u00e9sultat 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\u00e9 car le laptime ne devrait pas \u00eatre 2:15 mais 1:35... Voila apr\u00e8s 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 \u00e0 devenir long, il serait peut-\u00eatre int\u00e9ressant d'utiliser un seul Tesseract Engine ou de voir ce qui prend autant de temps, on d\u00e9passe la seconde de traitement ce qui est un peu ma limite. Apr\u00e8s on peut toujours tester de rajouter du multicore processing mais c'est pour une autre fois. Demain je m'occupe de r\u00e8gler les soucis que j'ai avec la prescision de ces temps au tour et j'\u00e9sp\u00e8re pouvoir m'occuper aussi de la position des pneus et du DRS. J'aimerais finir tout ca cette semaine.","title":"Mercredi 5 Avril"},{"location":"jdb.html#jeudi-6-avril","text":"Une id\u00e9e m'est pass\u00e9e par la t\u00eate pendant que je dormais, dans la liste des pilotes, quand ils sont \u00e0 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 \u00e9videmment un probl\u00e8me. Je pense qu'une bonne facon d'envoyer l'info serait de retourner -1 -2etc... \u00e0 la place des milisecondes, mais encore faut-il detecter le nombre de tours Je devrais \u00eatre 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\u00e9cup\u00e8r\u00e9 toutes les infos au moins un peu proprement. Cela veut dire que je commence officiellement \u00e0 prendre du retard. (Sachant que si je finis tout aujourd'hui une journ\u00e9e de doc suffira largement le terme est un peu exag\u00e8r\u00e9 mais bon) Bon pour la reconnaissance des temps c'est sp\u00e9cial... Le filtre semble ne pas changer grand chose ce qui est probl\u00e9matique et ca n'est vraiment pas fiable. Voici quelques expemples avec un treshold de 100: \"11ZSD\" Cette image est comprise comme \"11ZSD\" 42340 Cette image est comprise comme \"42340\" \"ZZAEB\" Et celle ci \"ZZAEB\"... Ce qui... n'est pas bon du tout... J'ai essay\u00e9 de trouver un fichier d'entrainement sp\u00e9cifiquement fait pour les digits. J'ai essay\u00e9 de blacklister les chars non voulus pour tenter d'obliger Tesseract \u00e0 trouver des chiffres. Avec la premi\u00e8re option, les r\u00e9sultats ne sont pas meilleurs voire pires. Avec la seconde option c'est d\u00e9ja pas mal mieux mais on perd compl\u00e8tement la possibilit\u00e9 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\u00e9es 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\u00e9 d'utiliser de l'interpolation our augmenter la taille de l'image et ensuite appliquer mon filtre pour retirer le flou mais sans succ\u00e8s... Pourtant la on se retrouve avec des images plut\u00f4t claires : \"Clear1\" Ici le programme trouve \"44301\" \"Clear2\" Et ici \"A5151\"... On a toujours les m\u00eames probl\u00e8mes. Bon je suis all\u00e9 me renseigner sur l'OCR et je me suis dit que j'allais tenter de faire les choses proprement. Je vais faire passer plusieurs \u00e9tapes de postProcessing avant de donner l'image \u00e0 Tesseract. GrayScale Tresholding InvertColors Scaling Dilatation Ce qui donne : \"Original\" \"Grayscale\" \"InvertColors\" ; \"Resize\" ; \"Dilatation\" Ce qui ne change : Roulement de tambour RIEN kjd viuwvuirnvoirenbf Tout ca pour rien... C'EST BON !!! Bon en fait au final le probl\u00e8me \u00e9tait une mauvaise configuration de Tesseract. Je vais devoir un peu nettoyer tout ca. Mais avec les changements de l'image on a des r\u00e9sultats BEAUCOUP plus pr\u00e9cis et potentiellement utilisables. La je vais devoir faire un serieux travail de nettoyage et simplification de mon code par ce que la c'est vraiment un chantier vu le nombre de choses que j'ai du essayer. J'ai du aussi beaucoup modifier la gestion de l'image ce qui donne : \"Clean\" Et la on a des r\u00e9sultats 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\u00e9mentation de la reconnaissance du DRS et des Pneus Et non... je viens de me rendre compte que mon programme a encore cass\u00e9 car le tap time ne peut pas \u00eatre 23 min lol. J'ai un nouveau magnifique probl\u00e8me... Les points et les deux points sont interpr\u00eat\u00e9s comme des chiffres ... Give me a F * * break... J'ai du mal \u00e0 comprendre pourquoi ils ne sont d\u00e9tect\u00e9s comme tels que maintenant. Bon alors il semblerait les temps au tour aie besoin d'un ordre tr\u00e8s pr\u00e9cis pour fonctionner. Grayscale InvertColors Tresholding Resize * 2 Resize * 2 Et la on a des r\u00e9sultats un peu mieux. Bon demain il faut absolument que je me charge de r\u00e8gler tous ces probl\u00e8mes et que je commence la reconnaissance des pneus et de DRS par ce que je commence \u00e0 \u00eatre en retard.","title":"Jeudi 6 Avril"},{"location":"jdb.html#vendredi-6-avril-2023","text":"Alors aujourd'hui c'est le dernier jour avant de commencer \u00e0 \u00eatre en retard pour de bon. J'ai r\u00e9ussi \u00e0 r\u00e8gler le probl\u00e8me des temps au tour, des gaps, et des secteurs. Dans le processus j'ai cass\u00e9 la detection de position mais ca devrait pas \u00eatre TROP compliqu\u00e9. Et voila ... Apr\u00e8s seulement plus de dix heures de gal\u00e8re, 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\u00f4t bien pour le DRS est que je prend la premi\u00e8re image de DRS et je la d\u00e9clare comme valeur \u00e9talon 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 \u00e0 moins de deux secondes les uns des autres pour confirmer). Ensuite cette valeur \u00e9talon 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\u00e9 ex: Ceci est un DRS ferm\u00e9: \"Closed DRS\" Ceci est un DRS ouvert: \"Open DRS\" Cela marche \u00e0 peu pr\u00e8s 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\u00eatant d'utiliser GetPixel et SetPixel qui sont simplement des horreurs \u00e0 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 \u00e9volue tout le long du Grand Prix. Bref c'est la gal\u00e8re. En y r\u00e9flechissant je me suis dit qu'une bonne id\u00e9e pourrait \u00eatre de partir de la droite de la zone du pneu en regardant au milieu de la hauteur. Puis continuer vers la gauche jusqu'\u00e0 ce que je rencontre une couleur diff\u00e9rente. 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\u00e9termin\u00e9 que le background n'\u00e9tait jamais plus clair que #505050 et que donc nimporte quelle couleur qui aurait plus que 50 dans un seul des channels serait consid\u00e8r\u00e9e comme une couleur cassant le background Pour arriver \u00e0 cette conclusion je me suis amus\u00e9 un peu avec les couleurs pour jouer avec les limites de mon algorythme : \"Color fun\" Et je crois que j'ai eu une bonne id\u00e9e, avec une petite methode bien faite on arrive \u00e0 de supers r\u00e9sultats : private Rectangle FindTyreZone() { Bitmap bmp = WindowImage; int currentPosition = bmp.Width; int height = bmp.Height / 2; Color limitColor = Color.FromArgb(0x50,0x50,0x50); Color currentColor = Color.FromArgb(0,0,0); Size newWindowSize = new Size(bmp.Height,bmp.Height); while(currentColor.R <= limitColor.R && currentColor.G <= limitColor.G && currentColor.B <= limitColor.B && currentPosition > 0) { currentPosition--; currentColor = bmp.GetPixel(currentPosition,height); } //Its here to let the new window include a little bit of the right side int offset = Convert.ToInt32((float)newWindowSize.Width / 100f * 20f); int CorrectedX = currentPosition - (newWindowSize.Width - offset); if (CorrectedX <= 0) return new Rectangle(0,0,newWindowSize.Width,newWindowSize.Height); return new Rectangle(CorrectedX,0,newWindowSize.Width,newWindowSize.Height); } \"Tyres\" Maintenant cela devrait \u00eatre beaucoup plus simple de trouver la couleur g\u00e9n\u00e9rale et le nombre de tours. Donc ce que je fais c'est que je fais une reconnaissance de texte sur l'image r\u00e9duite. 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\u00e9rentes couleurs de pneus : SOFT : #FF0000 MEDIUM : #f5bf00 HARD : #d9d8d4 INTER : #00a42e WET : #2760a6 \"Tyre colors\" Les couleurs de pneus peuvent changer de temps \u00e0 autres, par exemple cette r\u00e8gle de pneus est arriv\u00e9e en 2019 et avant il y avait beaucoup plus de couleurs mais dans une volont\u00e9 de rendre le sport plus facile \u00e0 comprendre \u00e0 la t\u00e9l\u00e9 cela a \u00e9t\u00e9 simplifi\u00e9. Je ne pense pas que cela va changer dans les ann\u00e9es qui viennent alors tout est hardcod\u00e9. Je pense que j'ai des soucis avec la detection de texte et de couleur car ma zone est trop grande. Alors bon j'\u00e9crit ces lignes apres des heures de tests. Il semble que la principale difficult\u00e9 avec ces pneus c'est que les chiffres ou lettres sont minuscules. Il est donc extr\u00eamement difficile de faire une reconnaissance ne serait-ce qu'un peu fiable.. Je fais de mon mieux pour tenter de r\u00e8gler le soucis cependant c'est vraiment complexe. Je commence \u00e0 devenir fou, je tente tout et nimporte quoi pour permettre \u00e0 mon algo de fonctionner et m\u00eame quand je fais du post processing comme pas possible il me retourne toujours nimporte quoi... \"5i t'inqui\u00e8tes\" 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\u00e9flechir 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.","title":"Vendredi 6 Avril 2023"},{"location":"jdb.html#vacances","text":"Bon je vais un peu laisser de c\u00f4t\u00e9 la detection de chiffres pour me pencher un peu plus sur la d\u00e9tection de couleur. Par ce que techniquement si j'arrive \u00e0 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\u00e9ussi \u00e0 fix mon probl\u00e8me de mauvaise detection de couleur de pneus. Du moins je crois. Seulement j'ai quand m\u00eame un souci, les fen\u00eatres de pneus avec une lettre n'ont pas assez de couleur pour \u00eatre d\u00e9tect\u00e9s. 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\u00e9duire les possibilit\u00e9s de Tesseract. Je me rend de plus en plus compte que le plus important c'est de r\u00e9duire 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\u00eat\u00e9 comme trois I ou comme un M... J'en ai marre sans rire c'est vraiment p\u00e9nible. Alors j'\u00e9crit ces lignes deux jours plus tard et me rend compte avec horreur que toutes mes modifications sur ce journal de bord n'ont pas \u00e9t\u00e9 auvegard\u00e9e... yess.. Bon pour faire simple, j'ai r\u00e9ussi \u00e0 rendre la detection de couleurs bien plus efficace en r\u00e9duisant la taille de l'image et en ne prenant pas en compte les couleurs que l'on d\u00e9tecte comme \u00e9tant partie int\u00e9grante du background. Par exemple quand on a une image comme celle ci : \"Avec background\" qui contient un background alors que ci dessous, on l'a enlev\u00e9. \"Sans background\" La diff\u00e9rence est t\u00e9nue mais elle permet de grandement am\u00e9liorer la prescision de la reconnaissance de couleurs. Pour ce qui est du nombre de tours je me suis rendu compte que cela n'\u00e9tait d\u00e9ja pas tr\u00e8s utile car avec l'historique on devrait pouvoir le d\u00e9duire. Mais bon pour la forme je me suis dit que cela serait quand m\u00eame une bonne id\u00e9e de v\u00e9rifier avec la reconnaissance. J'\u00e9tais quasi certain que le soucis \u00e9tait 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 \u00e9t\u00e9 simple) J'ai pu avoir des chiffres beaucoup plus proches de la r\u00e9alit\u00e9. En m\u00eame temps je ne vois pas bien comment j'aurais pu faire mieux : \"Super 11\" Je suis quand m\u00eame assez fier de voir que j'ai r\u00e9ussi \u00e0 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\u00e9zoomer un peu et c'est avec ca que les lettres ont pu \u00eatre mieux reconnues : \"Super H\" \"Super M\" Maintenant je pense qu'il ne reste \"plus qu'\u00e0\" nettoyer un peu tout ce code qui traine de partout pour tout faire fonctionner et impl\u00e9menter 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'\u00e9t\u00e2t, le programme met 25 secondes \u00e0 d\u00e9marrer et consomme presque 2GB de Ram. Certes cela ne veut pas dire que la reconnaissance \u00e0 elle seule prend 25 secondes car au d\u00e9marrage il y a aussi la lecture du fichier de config et la cr\u00e9ation des window etc.. En r\u00e9alit\u00e9 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 \u00eatre possible mais en tout cas le but va \u00eatre de s'en rapprocher. Pour \u00eatre 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 \u00e0 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\u00e9marrage 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\u00e9s 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\u00e9 d'utiliser un Parrallel.For au moment de la cr\u00e9ation des windows, le probl\u00e8me c'est que visiblement les objets windows sont beaucoup trops complexes et utilisent trop de ressources partag\u00e9es pour \u00eatre vraiment thread Safe. J'\u00e9sp\u00e8re que je n'aurais pas trop de soucis avec ca qu'en j'en viendrai \u00e0 l'optimisation de l'OCR... Ce qui me rend fou c'est que cette boucle toute nulle prend plus de dix secondes \u00e0 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 \u00eatre ca qui prend du temps. Et les windows que je cr\u00e9\u00e9 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\u00e9cile... J'ai juste \u00e0 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\u00e8s belle am\u00e9lioration pour pas tr\u00e8s ch\u00e8r mais bon c'est un peu b\u00eate... Bon je pense que 2-300ms c'est une dur\u00e9e correcte surtout que ca n'est appel\u00e9 qu'une fois pour le lancement. On peut passer \u00e0 la suite maintenant. Alors il y a un grand soucis avec la parallellisation de l'OCR... Tesseract n'est pas par d\u00e9faut une classe \"Thread safe\" ce qui veut dire que je ne peut utiliser de parallell.Foreach sur mes windows pour acc\u00e8l\u00e8rer 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 \u00e0 vue, je me base sur diff\u00e9rentes infos que je trouve sur des sites un peu perdus et sur chatGPT, j'esp\u00e8re que j'arriverai \u00e0 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 \u00e0 la fois. Je vais donc retirer l'engine unique pour voir si en cr\u00e9er un par window me permet de passer en multithreading. La grande question sera : Est-ce que les ressources suppl\u00e9mentaires que vont prendre la cr\u00e9ation de tous ces engines va compenser enti\u00e8rement le temps gagn\u00e9 avec la paralellisation. Pour stocker les donn\u00e9es dans un premier temps je vais cr\u00e9er un objet DriverData. Ce qu'il y a de pratique avec ca, c'est que je pourrais ajouter du code de v\u00e9rification de certaines donn\u00e9es directement dedans avant de les donner \u00e0 la suite du programme. Et on peut m\u00eame imaginer une impl\u00e9mentation d'une liste de DriverData pour avoir l'historique. Ce qui serait cool ca serait de grouper toutes ces data avec un num\u00e9ro 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\u00e9 sur la page de la F1TV n'est accompli que par certains des premiers pilotes. D'autres pilotes peuvent \u00eatre dans des tours pr\u00e9c\u00e9dents si ils ont du retard. Il faudra r\u00e9fl\u00e9chir \u00e0 cela quand je viendrai au mod\u00e8le. Bon pour y arriver j'ai du faire de gros changements et le r\u00e9sultat n'est peut-\u00eatre pas aussi cool que ce que j'aurais voulut... Voici un petit point sur les performances maintenant J'ai \u00e9galement d\u00e9sactiv\u00e9 le dump d'images. Pour le moment j'ai tout mis en commentaire mais cela pourrait \u00eatre int\u00e9ressant de faire en sorte de pouvoir l'activer en changeant une ou deux variables Au d\u00e9marrage : Loading - 113ms Splitting d'images - 14ms Ocr - 7s En passant d'une image \u00e0 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\u00e9ation d'engines d'\u00eatre \u00e0 l'origine de ces performances presque d\u00e9cevantes. 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\u00e9 de mettre un stopwatch sur une des cr\u00e9ations d'engine Tesseract et le r\u00e9sultat me parait fou... Plus d'une seconde c'est dingue. J'ai test\u00e9 dans d'autres endroits du code et effectivement il semble que la cr\u00e9ation d'un engine prenne entre une et deux secondes ce qui est une ETERNITEE what ! Donc il faut optimiser tout ca. Une id\u00e9e serait de d\u00e9composer 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\u00e9\u00e9s ils ne prennent pas de temps du tout. Cr\u00e9er une fois tous les engines et ensuite les utiliser pourrait \u00eatre une bonne id\u00e9e. Cela prendrait longtemps au load mais ensuite les reconnaissances devraient \u00eatre super rapides. Ok alors ca c'est d\u00e9ja plus ce \u00e0 quoi je m'attendais ! On est de nouveau \u00e0 plus de 10s de loading time mais on est descendu \u00e0 deux secondes par OCR. (Bon autre soucis, l'utilisation de la RAM est ridicule plus de 2gb mais ce qui m'inqui\u00e8te c'est que j'ai l'impression qu'elle augmente plus on change d'image) J'ai r\u00e8gl\u00e9 (en partie) le soucis en obligeant le GC (Garbage Collector) \u00e0 collecter apr\u00e8s chaque detection. m\u00eame apr\u00e8s 50 detections l'utilisation de la ram se stabilize autour des 2GB. Bon en paralellisant la cr\u00e9ation des Engines le soucis c'est que cela demande d'allouer beaucoup trop de m\u00e9moire d'un coup alors le programme se fige pendant genre cinq secondes avant de tout cr\u00e9er. Du coup m\u00eame si la cr\u00e9ation 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\u00e9ale... J'ai r\u00e9ussi \u00e0 changer un petit peu le programme de reconnaissance pour rendre la reconnaissance un peu meilleure mais cela a drastiquement augment\u00e9 le temps requis pour d\u00e9coder... On arrive \u00e0 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 \u00e0 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\u00eater la pour la partie d\u00e9codage. 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 \u00e0 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\u00e9ressante du projet et ou je pense que j'aurai le plus essay\u00e9 de choses qui vallent le coup d\u00eatres racont\u00e9es. J'ai aussi pass\u00e9 pas mal de temps sur le poster du projet. J'avais fait des croquis au crayon de ce \u00e0 quoi je pensais, cependant apr\u00e8s de longues discussions avec M.Garcia ils n'\u00e9taient pas forc\u00e9ment tr\u00e8s bons car ils ne repr\u00e9sentent pas assez bien le fonctionnement du projet et sont un peu trop marketings. Du coup j'ai fait une premi\u00e8re version au propre : \"Poster V1\" Mais je n'\u00e9tais pas forc\u00e9ment content du r\u00e9sultat et il manquait des choses je trouve comme par exemple l'utilisation de Selenium. J'ai donc repass\u00e9 des heures \u00e0 faire une seconde version : \"Poster V2\" La police d'\u00e9criture n'est pas encore la bonne mais cela va venir. Mais je pr\u00e9f\u00e8re d\u00e9ja beaucoup cette version \u00e0 la premi\u00e8re. Je ne sais pas encore si la version finale sera une version plus travaill\u00e9e de ce poster ou compl\u00eatement autre chose mais pour l'instant je suis \u00e0 peu pr\u00e8s content de cette version. Je le trouve un tout petit peu trop brouillon ou avec trop d'infos mais il m'a \u00e9t\u00e9 de nombreuses fois reproch\u00e9 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 \u00e0 peu pr\u00e8s 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\u00e9\u00e9 u nouveau projet selenium mais m\u00eame avec les bonnes libraries je n'arrivais pas \u00e0 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\u00e8gler le probl\u00e8me en t\u00e9l\u00e9chargeant 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\u00e8me c'est que du coup il faut tout le temps d\u00e9placer 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\u00e9cup\u00e9ration de cookie. J'ai d\u00e9ja 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 \u00e9cran et un programme qui prend des captures d'\u00e9crans. Avoir une cam\u00e9ra qui prend en photo l'\u00e9cran au cas ou chrome et Firefox emp\u00eachent la prise de captures d'\u00e9crans. R\u00e9cup\u00e8rer directement le feed en faisant du reverse engeneering de la plateforme. Simuler un chrome en background qui prenne des screenshot sans qu'on aie \u00e0 le voir. Dans toutes ces options, je dirais que la pire \u00e9tait celle de la cam\u00e9ra qui filme l'\u00e9cran, mais \u00e0 l'\u00e9poque c'\u00e9tait 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\u00e8s mauvaise qualit\u00e9. Une autre option qui m'aurait vraiment emb\u00eat\u00e9 aurait \u00e9t\u00e9 de devoir garder une page de Chrome ou Firefox ouverte quelque part sur un \u00e9cran pour que le programme puisse prendres des captures d'\u00e9crans. C'est de loin l'option la plus simple et la plus logique mais elle poss\u00e8de pour moi de tr\u00e8s gros points noirs : On ne peut pas certifier l'int\u00e9grit\u00e9 des donn\u00e9es car l'utilisateur a le contr\u00f4le total sur le feed. Il peut mettre pause, avancer, reculer, tout casser sans faire expr\u00e8s 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'\u00e9cran 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\u00e8tre d'\u00e9cran est crucia\u00e9 ! Alors avoir un \u00e9cran complet bloqu\u00e9 est juste un point bloquant qui m'emp\u00eacherait d'utiliser l'app aussi bonne soit-elle dans ses pr\u00e9dictions. Mais bon si aucune autre methode ne fonctionne ce qui est bien c'est que celle la est plut\u00f4t simple \u00e0 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\u00e9gal haha. L'id\u00e9e serait de r\u00e9cup\u00e8rer le lien vers le broadcast g\u00e9n\u00e9ral et de comprendre comment il fonctionne pour le d\u00e9coder nous m\u00eame 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\u00e9riodes ou je travaille en plus) Difficile de faire un syst\u00e8me qui marche pareil pour les rediffusions et les lives. (En effet les liens des rediff sont beaucoup plus simple \u00e0 r\u00e9cup\u00e8rer 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-\u00eatre que avec des profs qui m'aident et chat gpt ainsi qu'internet je pourrais potentiellement n\u00e9gocier un truc mais c'est hautement improbable et cela serait une perte de temps folle si je n'y arrive pas. Derni\u00e8re option que je trouve la plus s\u00e9duisante. Simuler une instance de Chrome ou de Firefox (Le soucis avec chrome c'est qu'il impl\u00e9mente l'utilisation de DRM dans les vid\u00e9os qui fait qu'il est tr\u00e8s difficile de passer outre la s\u00e9curit\u00e9 avec un bot) pour ensuite prendre des captures d'\u00e9crans automatiquement. Cette solutions offre pleins d'avantages : Pas de place prise sur l'\u00e9cran L'int\u00e9grit\u00e9 des donn\u00e9es est assur\u00e9e car c'est le programme qui d\u00e9cide 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\u00e9matiques : Comment se connecter automatiquement sans \u00eatre detect\u00e9 par un Bot et sans demander \u00e0 l'utilisateur ses identifiants (Pour des raisons \u00e9videntes 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\u00e9 et en plein \u00e9cran. Mais j'ai d\u00e9cid\u00e9 de partir sur cette option. Pour ce faire j'utilise Selenium. J'ai pu tester Puppetteer Sharp et m\u00eame 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\u00e9cid\u00e9 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\u00e9sactive le \"Headless\" pour qu'on puisse voir ce que fait l'app car sinon tout est invisible. Alors dans les faits la vid\u00e9o 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\u00e9rents challenges que l'on va devoir surmonter pour vraiment faire ce que l'on veut. Mais un petit d\u00e9tail extr\u00eamement important, la F1TV est un programme payant un peu comme netflix. Ce qui veut dire que pour acc\u00e8der au contenu il faut \u00eatre connect\u00e9. Sauf que une instance de firefox cr\u00e9\u00e9 par Selenium est comme une page de naviguation priv\u00e9e, ce qui veut dire que si on va sur la page de la F1TV on est pas connect\u00e9s. Je pourrais tout \u00e0 fait demander \u00e0 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 \u00e9t\u00e9 prot\u00e8g\u00e9e avec la meilleure technologie de detection de bots que je connaisse. Presque aucun site n'arrive \u00e0 me detecter sauf eux ! Donc c'est tout simplement impossible d'utiliser cette technique. Ensuite je me suis rappel\u00e9 que ce que la page stocke pour me permettre de rester connect\u00e9 ce sont des cookies. Et si je mets le bon cookies dans Selenium alors je serai connect\u00e9. Dans un premier temps je voulais faire un syst\u00e8me 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\u00e9cup\u00e8ration de cookies ! Tous les cookies de chrome sont stock\u00e9s dans une base de donn\u00e9es SQLITE. On pourrait se dire Banco il suffit d'aller dedans et de retrouver tous les cookies et se connecter. Sauf que, pas b\u00eates, les \u00e9quipes de chrome ont d\u00e9cid\u00e9 que c'\u00e9tait une bonne id\u00e9e 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\u00e9coder. Mon hypoth\u00e8se a \u00e9t\u00e9 que si ces cookies peuvent \u00eatre lus par Chrome m\u00eame hors connexion, c'est que la cl\u00e9 de d\u00e9codage existe sur l'appareil et qu'il suffit de la trouver. ET C'EST LE CAS! Apr\u00e8s pas mal de recherches j'ai pu voir que la cl\u00e9 de d\u00e9codage existe bel et bien et qu'il suffit de la d\u00e9coder en utilisant la librairie DPAPI pour la lire. Avec cette cl\u00e9 on peut ensuite d\u00e9coder les cookies et leurs valeurs ce qui veut dire qu'il est th\u00e9oriquement possible d'automatiser le processus sans que l'utilisateur n'aie rien \u00e0 faire. J'ai d\u00e9cid\u00e9 de faire la partie r\u00e9cup\u00e8ration en python pour deux raison : Je n'arrivais pas \u00e0 trouver une bonne impl\u00e9mentation de DPAPI en C# qui me permettait de d\u00e9coder la cl\u00e9. 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 \u00e0 recompiler le code C#. J'ai donc avec l'aide d'internet et de ChatGPT cr\u00e9\u00e9 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\u00e9 et va ensuite stocker les r\u00e9sultats dans un CSV pour qu'il soit facile d'y acc\u00e8der depuis le C#. Alors oui cela pose certaines questions de s\u00e9curit\u00e9. Car en effet je prend tous les cookies, les d\u00e9code et les stocke. Ce qui veut dire que je pourrais tout \u00e0 fait envoyer ces donn\u00e9es 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\u00e9 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\u00f4t\u00e9 j'ai juste \u00e0 lire le CSV et le tour est jou\u00e9 ! (Trouver cette solution m'a pris une semaine de vacances \u00e0 l'\u00e9poque) Bon j'ai r\u00e9ussi \u00e0 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\u00e9. Pour changer la qualit\u00e9 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\u00eame. Elle commence toujours pas \"1080_\" mais ensuite ca peut \u00eatre \"1080_45930285\" ou \"1080_56801\" la suite est apparemment random. J'ai donc du utiliser ce code pour le selectioner quand m\u00eame : 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\u00e8me c'est qu'il est invisible alors on doit le faire apparaitre. J'ai tent\u00e9 de le faire aparaitre en bougeant la souris, en cliquant \u00e0 un endroit pr\u00e9cis, impossible de le faire marcher correctement. Puis j'ai eu l'id\u00e9e de mettre pause en envoyant un appui sur la touche Espace et ca a permit de d\u00e9couvrir le bouton et permettre qu'on clique dessus. Ca peut paraitre tout b\u00eate mais rien que ca, ca m'a pris un temps consid\u00e9rable. Bon pour ce qui est du timecode de la vid\u00e9o. Je pense qu'il serait trop complexe de faire en sorte que selenium change le slider de progression de la vid\u00e9o. Alors j'ai fait quelques tests et apparemment, si on quitte la F1TV sur un timecode de la vid\u00e9o que on donne au programme, comme il r\u00e9cup\u00e8re tous les cookies de la F1TV il commencera de la. Donc si on veut utiliser le programme avec des Grand Prix ayant d\u00e9ja eu lieu, on peut le faire, seulement il faudra juste au pr\u00e9alable avoir choisit le bon timecode dans le page de la F1TV avant de le lancer. Ce qui est int\u00e9ressant c'est que la page de la F1TV ressemble \u00e0 ca au d\u00e9part : \"Empty F1TV\" Je pense qu'une bonne id\u00e9e serait de dire au programme que c'est la grille de d\u00e9part et ensuite d\u00e8s qu'il d\u00e9tecte un secteur il sait que la course a commenc\u00e9.","title":"Vacances"},{"location":"jdb.html#lundi-24-avril-2023","text":"Aujourd'hui c'est jour de documentation. J'ai pas mal travaill\u00e9 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-\u00eatre un peu demain. Ok normalement je ne devrais faire que de la documentation mais je ne peux pas passer \u00e0 cot\u00e9 de ca... Le probl\u00e8me que j'ai avec les pneus ou parfois il d\u00e9tecte un H au lieu d'un '11' et ce genre de choses c'est \u00e0 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\u00eame et qui va donc defigurer les 11 : \"diformed 11\" \"diformed 11\" J'ai r\u00e9ussi \u00e0 les changer en : \"less diformed\" \"less diformed\" Mais au final cela n'a pas augment\u00e9 la pr\u00e9cision 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\u00eater de chercher des lettres ou des chiffres. Comme ca les 11 arr\u00eateront d'\u00eatre 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\u00eame si il y a une lettre sur fond noir pour d\u00e9crire le pneu, mon methode de r\u00e9cup\u00e8ration de la couleur dominante ommet les pixels trop noirs alors il est quand m\u00eame possible de determiner le type de pneus. Et tout simplement si il n'arrive pas \u00e0 lire le chiffre c'est que c'est une lettre et que donc on est \u00e0 0 tours. Cela marche plut\u00f4t bien et cela simplifie pas mal le processing. Voila, la je vais me remettre \u00e0 la documentation sinon je vais encore prendre du retard.","title":"Lundi 24 Avril 2023"},{"location":"jdb.html#mardi-25-avril-2023","text":"Encore une fois j'ai pris du temps de doc pour changer des choses sur la partie OCR. Mais en m\u00eame temps en documentant je vois des choses que j'ai soit mal fait soit que je pourrais faire mieux en changeant tr\u00e8s peu de choses. J'\u00e9sp\u00e8re que les changement que j'ai fait vont aider au moins \u00e0 la coh\u00e9rence du code et un peu pour les performances. Il semble que dans les conditions que j'ai test\u00e9 le nombre de tour soit plut\u00f4t fiable mais je pense que je devrai faire un peu de travail en aval dans la r\u00e9cup\u00e9ration de ces donn\u00e9es car je sens que cela va poser probl\u00e8me quelques fois. Je pense que en utilisant bien l'historique on peut potentiellement se passer de l'utilisation de ce chiffre pas toujours compl\u00eatement fiable. Mais sinon aujourd'hui c'est encore une fois un gros jour de doc. J'essaie d'expliquer les diff\u00e9rents proc\u00e9d\u00e9s avant de les oublier. J'essaie aussi de donner un maximum d'exemples sous formes de photos interm\u00e9diaires 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\u00e9diaires. En plus de la documentation je me suis aussi beaucoup occup\u00e9 de nettoyer mon code et je suis assez content par ce que m\u00eame en ayant du rajouter des couches de complexit\u00e9 pour mieux reconnaitres les temps au tour j'arrive \u00e0 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\u00e9 aujourd'hui et je sui bien mort. Faire autant de documentation et de nettoyage de code c'est pas forc\u00e9ment bon pour le cerveau je crois. J'ai besoin d'une sieste. Demain je pense que je vais commencer \u00e0 avancer sur la partie r\u00e9cup\u00e8ration des images. Je sais que la je fais un peu passer les tests \u00e0 la trappe mais d\u00e9ja j'en ai fait tout le long du d\u00e9veloppement de OCR_DECODE et il faut vraiment que j'avance, quitte \u00e0 revenir dessus quand j'aurai merge les deux projets ensemble.","title":"Mardi 25 Avril 2023"},{"location":"jdb.html#26-avril-2023","text":"Aujourd'hui je vais devoir m'occuper de la partie r\u00e9cup\u00e9ration des images. J'ai d\u00e9ja eu l'occasion d'avancer sur ce projet pendant mopn poc et mes vacances. Donc la le but ca va \u00eatre de voir ce qui manque comme v\u00e9ritables features et ensuite je vais pouvoir m'occuper de la vue et de son int\u00e9gration avec le d\u00e9codage. Ok donc maintenant que j'au un programme qui arrive \u00e0 prendre des images depuis la F1TV correctement et en bonne r\u00e9solution. Je pense qu'il est temps de passer \u00e0 l'impl\u00e9mentation de la Forme que ca va prendre. C'est important de se poser au moins cinq minutes la question de comment je pr\u00e9vois de faire car m\u00eame si ca n'est pas la version finale, cette derni\u00e8re prendra tr\u00e8s fort inspiration du desing que je vais faire. Dans cette form j'aurais besoin de : Pouvoir selectionner un Grand Prix en ins\u00e8rant 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\u00e9ja un certain nombre de tours lanc\u00e9s. Et c'est \u00e0 peu pr\u00e8s tout en fait... J'ai tellement pouss\u00e9 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\u00e9menter la calibration qui elle aura besoin d'une UI un peu plus bal\u00e8ze. On pourrait m\u00eame imaginer que la calibration fasse partie int\u00e9grante des settings... Ca serait peut-\u00eatre bien que quand l'application se lance on se retrouve sur la page principale d'affichage de donn\u00e9es 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\u00e9velopp\u00e9e pour regrouper tout ca : \"Screen\" La police le style le placement et les couleurs ne sont pas d\u00e9finitfs, cependant je pense que c'est un bon d\u00e9but. Le but maintenant va \u00eatre de permettre de faire fonctionner la calibration et la r\u00e9cup\u00e8ration d'images. Si j'arrive \u00e0 faire fonctionner ces deux choses sur un m\u00eame projet avant la fin de la semaine cela serait super ! Bon J'ai pu avancer sur l'int\u00e8gration de Selenium mais cela prend un peu de temps car je veux impl\u00e9metner un moyen de pouvoir prendre une Screenshot \u00e0 nimporte quel moment et pas juste en boucle. Demain je finis de faire fonctionner ca et ensuite je commence le cablage du reste.","title":"26 Avril 2023"},{"location":"jdb.html#jeudi-27-avril-2023","text":"C'est assez dur de faire l'importation car il y a des petites diff\u00e9rences qui obligent \u00e0 presque tout r\u00e9\u00e9crire. En fait le programme de calibration avait d\u00e9ja impl\u00e9ment\u00e9 la fonction de Windows et de Zones mais il fonctionnait juste assez diff\u00e9remment pour qu'il faille tout refaire. La je suis en train de perdre \u00e9norm\u00e9ment de temps \u00e0 cause d'un soucis de coordonn\u00e9es. J'ai repris le code de la calibration pour detecter ou l'utilisateur a cliqu\u00e9 pour cr\u00e9er les zones. Cependant, je n'arrive pas \u00e0 le faire fonctionner correctement. La zone est tout le temps d\u00e9cal\u00e9e en haut et en bas mais pas de la m\u00eame 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'\u00e9tait un simple d\u00e9calage cela ne serait pas compliqu\u00e9 \u00e0 g\u00e8rer mais la... J'ai un soucis \u00e9galement avec la r\u00e9solution des screenshots que je r\u00e9cup\u00e8re en full Headless. Voici un exemple de r\u00e9solution que j'arrive \u00e0 r\u00e9cup\u00e8rer sans le headless : \"High Res\" \"Low Res\" Il y a clairement un soucis et le probl\u00e8me c'est que avec une r\u00e9solution pareille, impossible de faire une reconnaissance correcte. BON J?EN PEUX PLUS LA. Ca fait des heures que je bosse sur ce probl\u00e8me d\u00e9bile et impossible de trouver une solution. J'ai essay\u00e9 cinq facons de forcer le browser headless a prendre une plus haute r\u00e9solution aucune ne fonctionne je ne comprends pas. A chaque fois que je me retrouve avec une r\u00e9solution de 1366 x 768 Ou une variante de basse r\u00e9solution du style. J'en peux plus je ne trouve aucune r\u00e9ponse sur internet ni m\u00eame 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.","title":"Jeudi 27 Avril 2023"},{"location":"jdb.html#vendredi-28-avril-2023","text":"Une des solutions que je n'ai pas encore essay\u00e9 est de changer ma version de GeckoDriver. Sauf que ca m'oblige \u00e0 changer les versions de mes libraries ce qui est tr\u00e8s p\u00e9nible, je vais continuer le debugging dans le projet Selenium_clean. Il faut savoir que la librairie de Selenium que j'utilise est bloqu\u00e9e en 0.27 ce qui fait que je ne peux utiliser qu'une version obsol\u00e8te du Gecko Driver. J'ai tent\u00e9 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\u00e9solution DPI, j'essaie de changer les param\u00eatres par d\u00e9faut des player de Firefox, j'essaie de changer la r\u00e9solution pendant et au d\u00e9but de l'execution IMPOSSIBLE DE FAIRE MARCHER CETTE MERDE C'EST PAS POSSIBLE !!! J'ai essay\u00e9 avec chrome mais je ne peux pas l'utiliser car les DRM m'emp\u00eacheront de prendre des screenshot du flux vid\u00e9o. J'ai essay\u00e9 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 \u00e9nerv\u00e9 ca se voit? A bon? Et maintenant NUGGET ne fonctionne plus... j'en peux plus la. Je ne peux plus t\u00e9l\u00e9charger de librairie sur aucun de mes projets... J'ai tent\u00e9 de supprimer le fichier de config et red\u00e9marrer Visual Studio mais cela ne fait rien. J'ai aussi tent\u00e9 de faire un 'nugget restore' toujours rien. Bon apparemment je ne suis pas le seul qui ne peut pas acc\u00e8der \u00e0 Nuget donc bon c'est pas juste chez moi qu'il y a un soucis. Mais m\u00eame en mettant ma 4G pour me connecter, je n'arrive pas \u00e0 acc\u00e8der \u00e0 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\u00e9marr\u00e9 trois fois mon pc et visual studio, j'ai essay\u00e9 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\u00e9seau soit en meilleur \u00e9tat.","title":"Vendredi 28 Avril 2023"},{"location":"jdb.html#lundi-1-mai-2023","text":"Bon je bosse depuis chez moi donc j'esp\u00e8re que Nuget va mieux fonctionner. Apr\u00e8s un weekend \u00e0 r\u00e9fl\u00e9chir 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\u00e9solution de 1920x1200 comme moi. Cela veut donc s\u00fbrement dire que le soucis vient de cette r\u00e9solution de laptop comme moi. Si vraiment je n'arrive pas dans un premier temps \u00e0 faire fonctionner le Headless correctement, je peux toujours laisser la page de c\u00f4t\u00e9 et m'occuper du reste du programme. Certes ca serait vraiment infernal d'avoir \u00e0 garder une page chrome ouvert en tous temps et en plus elle doit \u00eatre en plein \u00e9cran mais bon... Si il n'y a vraiment pas d'autres solutions malheureusement je serai bien oblig\u00e9. BON ! JE N'ARRIVE MEME PLUS A FAIRE UN PROJET QUI UTILISE SELENIUM ET QUI MARCHE JE VAIS FAIRE BR\u00dbLER 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\u00eame putain de librairies que sur les autres projets les m\u00eames versions, je prend le m\u00eame exact code. Sur le nouveau projet impossible de le faire fonctionner. Je commence \u00e0 croire que on essaie de me faire p\u00eater un cable. Du coup dans un \u00e9lan de d\u00e9sespoir 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 \u00e0 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\u00eame 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\u00f4t partir sur la librairie GeckoFX qui semble pouvoir contr\u00f4ler une instance de firefox. Mais j'avais justement pris un putain de projet C# et pas JS pour ne pas me taper ces probl\u00e8mes de librairies... Et si cette option ne fonctionne pas mon dernier espoir sera de directement int\u00e9ragir avec le geckodriver.exe et la ca risque de pas \u00eatre dr\u00f4le. 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 \u00e0 rien je commence \u00e0 devenir fou. Tout ce travail pour rien c'est pas possible. M\u00eame en essayant directement d'int\u00e9ragir avec le process geckodriver.exe je ne peux pas arriver \u00e0 mes fins. J'arrive \u00e0 lancer le service et tout, mais je n'arrive pas \u00e0 vraiment contr\u00f4ler ce qu'il se passe donc impossible de venir prendre des screenshot. Je ne sais tout simplement pas quoi faire ... Je suis bloqu\u00e9. Je me suis cass\u00e9 la t\u00eate \u00e0 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 \u00eatre dr\u00f4le. Ok alors j'ai tout repris depuis le d\u00e9but et je crois que j'ai enfin une solution. Pour la trouver j'ai re-essay\u00e9 toutes les techniques que j'avais tent\u00e9 avant mais dans l'ordre et en les isolant \u00e0 chaque fois. Cela inclus : Tenter de changer la densit\u00e9 de pixels. En effet je me suis dit que comme la r\u00e9solution \u00e9tait plus basse le soucis \u00e9tait que le virtual screen avait simplement une DPI r\u00e9duite. profile.SetPreference(\"layout.css.devPixelsPerPx\", \"2.0\"); J'ai aussi tent\u00e9 de r\u00e9duire \u00e0 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\u00e9 tout b\u00eatement de rajouter dans la liste des arguments la taille voulue de l'\u00e9cran. 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\u00eatre \u00e0 \u00eatre plus grande. js.ExecuteScript(\"window.resizeTo(1920, 1080);\"); Comme cela n'a pas march\u00e9 j'ai pu lire que cela pouvait \u00eatre la taille int\u00e9rieure qui devait \u00eatre chang\u00e9e js.ExecuteScript(\"window.innerWidth = 1920; window.innerHeight = 1080;\"); Encore une fois sans succ\u00e8s. J'ai ensuite tent\u00e9 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\u00e9 quoi que ce soit \u00e9tait 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\u00eame temps, qui elle ne marchait pas mais qui emp\u00eachait 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'\u00e9tait pas normal alors je me disais que cette technique ne marchait pas non plus et je l'ai pass\u00e9e. Alors tout n'est pas encore gagn\u00e9, il faut que j'arrive \u00e0 impl\u00e9menter ca dans un plus gros projet et que la vid\u00e9o puisse \u00eatre prise seule. Demain je m'occupe de ca.","title":"Lundi 1 Mai 2023"},{"location":"jdb.html#mardi-2-mai-2023","text":"Bon aujourd'hui je change le programme principal. Le soucis que j'ai c'est que en ajoutant ce syst\u00e8me de resize, maintenant la page fait 100x100 et est grise. Il doit y avoir une technique que j'ai oubli\u00e9 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\u00e9l\u00e9charger EXACTEMENT les m\u00eames librairies que sur mon autre projet et j'utilise l'EXACT m\u00eame geckodriver.exe mais dans le projet principal impossible de lui faire chier une image m\u00eame avec l'EXACT m\u00eame 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\u00e8s avoir supprim\u00e9 l'int\u00e9gralit\u00e9 de ma classe Emulator cela semble marcher un peu mieux. Je ne vais pas m'\u00e9tendre sur la castrophe niveau temps que cela repr\u00e9sente. Si au moins j'arrive \u00e0 faire fonctionner quelque chose je suis content. Maintenant j'ai un soucis un peu sp\u00e9cial. Depuis que j'ai chang\u00e9 la r\u00e9solution, il semble que le programme aie du mal \u00e0 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 \u00e0 trouver le bouton : \"ERROR 105\" \"ERROR 105\" 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 \u00e9l\u00e9ment 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\u00e8me vienne du GP d'azerbidjan. En effet, quand je teste un autre Grand Prix tout va bien. ET MERDE ! J'ai r\u00e9ussi \u00e0 avoir des images en 1080P mais d\u00e9s que je passe l'image en plein \u00e9cran c'est de nouveau du 1366X768 Avant de mettre en plein \u00e9cran: \"Before fullscreen\" Apr\u00e8s: \"After fullscreen\" On peut voir sur l'image que l'option 1080P est effectivement bien selectionn\u00e9e mais il doit y avoir un param\u00e8tre de Firefox qui s'occupe de la r\u00e9solution d'un player vid\u00e9o. Il va juste falloir trouver ce param\u00eatre... J'ai essay\u00e9 d'utiliser : Driver.Manage().Window.Size = new System.Drawing.Size(windowWidth, windowHeight); Sans succ\u00e8s. 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\u00e9 la solution en trouvant cette commande profile.SetPreference(\"full-screen-api.enabled\", true); Mais non toujours pas... Je commence \u00e0 perdre patience. C'EST BON. Apr\u00e8s litt\u00e9rallement 3h de debugging avec M.Bonvin (Que je remercie IMMENSEMENT) on a r\u00e9ussi \u00e0 trouver au fin fond d'un thread github que la valeur \u00e9tait hard cod\u00e9e 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\u00e8gler a \u00e9t\u00e9 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.","title":"Mardi 2 Mai 2023"},{"location":"jdb.html#recrutement-payerne-mai-2023","text":"J'ai du faire mon recrutement \u00e0 Payerne Mercredi et Jeudi. Si vous \u00eates curieux je peux vous dire que comme il n'y avait presque plus de places cet \u00e9t\u00e9 je ferai Canonnier Lance mines. C'\u00e9tait assez frustrant d'avoir perdu deux jours de travail mais on va faire avec.","title":"Recrutement Payerne Mai 2023"},{"location":"jdb.html#vendredi-5-mai-2023","text":"Bon malgr\u00e9s les courbatures il faut que je me mette au boulot un peu serieusement par ce que sinon ca va \u00eatre compliqu\u00e9 de rattraper mon retard. La derni\u00e8re fois si je me souviens bien j'avais r\u00e9ussi \u00e0 trouver un moyen de prendres des images en bonne r\u00e9solution. Il faut maintenant que je commence \u00e0 faire fonctionner la calibration et ce qui serait bien ca serait que je commence \u00e0 ajouter la partie OCR au projet. Il faut que je me d\u00e9p\u00eache 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'\u00e9tait pas la bonne, l'image \u00e9tait recentr\u00e9e ce qui fait que cela faussait totalement les r\u00e9sultats. Quand on fait en sorte que l'image prenne toute la place, les coordonn\u00e9es sont prises correctement. Voici un exemple d'ou en est la partie calibration. \"Exemple settings UI\" Normalement il me suffit d'impl\u00e9menter les windows, et on devrait relativement facilement ajouter les pilotes. Et voila. J'ai pu impl\u00e9menter les windows et les pilotes. Et je peux aussi exporter des presets et les loader. Bon le loading est un peu beugg\u00e9 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 \u00eatre l'impl\u00e9mentation de l'OCR.","title":"Vendredi 5 Mai 2023"},{"location":"jdb.html#lundi-8-mai-2023","text":"Aujourd'hui c'est journ\u00e9e Poster. Je pense que je ne vais pas finir la journ\u00e9e content car les limitations sont un peu trop pr\u00e9sentes. J'ai fait une version que Garcia pourrait accepter, c'est \u00e0 dire en noir et blanc et avec un tout petit peu plus de d\u00e9tail. \"Poster V3\" 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-\u00eatre pas la meilleure id\u00e9e. Je me suis dit que la bonne id\u00e9e serait peut-\u00eatre de prendre un autre circuit pour qu'il y aie bien trois parties : \"Poster V4\" 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. \"Poster V5\" Je me suis ensuite dit que le circuit n'\u00e9tait peut \u00eatre tout simplement pas une bonne id\u00e9e. J'ai donc essay\u00e9 de faire quelque chose de plus classique avec juste un peu de background pour qu'on puisse \u00e9viter le soucis de la page blanche derri\u00e8re : \"Poster V6\" Puis je me suis dit que finalement le circuit me manquait. Alors j'ai d\u00e9cid\u00e9 de combiner le background et le circuit ainsi que simplifier l\u00e9g\u00e8rement les diagrammes en retouchant un peu tout le reste on pouvait arriver \u00e0 quelque chose de sympatique : \"Poster V7\" Je ne suis pas content \u00e0 100% mais bon je pense que je vais m'en satisfaire. Pour donner une id\u00e9e de la gal\u00e8re que c'est de cr\u00e9er un poster, voici ce \u00e0 quoi ressemble mon espace de travail Figma : \"Bordel 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\u00e9cup\u00e8ration d'images. En effet, je pense que je n'aurai plus besoin de changer grand chose \u00e0 ce niveau. Mais je ne ferai pas la partie analyse fonctionnelle car l'interface n'est clairement pas termin\u00e9e. En fait j'avais oubli\u00e9 mais j'ai eu un rendez vous m\u00e9dical 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 \u00e0 venir.","title":"Lundi 8 Mai 2023"},{"location":"jdb.html#mardi-9-mai-2023","text":"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 \u00e0 l'arm\u00e9e m'ont fait perdre un peu la notion du temps car j'avais l'impression que l'evaluation interm\u00e9diaire 1 \u00e9tait 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\u00e9cup\u00e8ration d'images. Je pense que je vais aussi ajouter la partie calibration \u00e0 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\u00e9 depuis la derni\u00e8re fois. Et puis je pense que la partie calibration et r\u00e9cup\u00e8ration 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\u00e9ja faite pendant les premiers jours alors ca devrait aller. Pour le rendu il nous \u00e9tait demand\u00e9 de fournir un fichier PDF avec tout dedans avec une table des mati\u00e8res 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: \u00a9CFPTI 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\u00e9 pour cette partie et avoir cr\u00e9\u00e9 un plugin qui me permet de mettre mon code source directement dans le pdf. Bon au final j'ai quand m\u00eame chang\u00e9 mon poster \"Poster V8\" Mais je suis trop attach\u00e9 \u00e0 l'ancien concept alors je vais plut\u00f4t utiliser ca : \"Poster V9\"","title":"Mardi 9 Mai 2023"},{"location":"Code/ConfigurationTool.html","text":"ConfigurationTool.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : ConfigurationTool.cs /// Brief : Class that contains all the methods needed to create a config file for the OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using Tesseract; using System.IO; namespace Test_Merge { public class ConfigurationTool { public Zone MainZone; public const int NUMBER_OF_DRIVERS = 20; public const int NUMBER_OF_ZONES = 9; public const string CONFIGS_FOLDER_NAME = \"./Presets/\"; public ConfigurationTool(Bitmap fullImage, Rectangle mainZoneDimensions) { MainZone = new Zone(fullImage, mainZoneDimensions,\"Main\"); AutoCalibrate(); } public void ResetMainZone() { MainZone.ResetZones(); } public void ResetWindows() { MainZone.ResetWindows(); } public void SaveToJson(List<string> drivers, string configName) { string JSON = \"\"; JSON += \"{\" + Environment.NewLine; JSON += MainZone.ToJSON() + \",\" + Environment.NewLine; JSON += \"\\\"Drivers\\\":[\" + Environment.NewLine; for (int i = 0; i < drivers.Count; i++) { JSON += \"\\\"\" + drivers[i] + \"\\\"\"; if (i < drivers.Count - 1) JSON += \",\"; JSON += Environment.NewLine; } JSON += \"]\" + Environment.NewLine; JSON += \"}\"; if (!Directory.Exists(CONFIGS_FOLDER_NAME)) Directory.CreateDirectory(CONFIGS_FOLDER_NAME); 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); } public void AddWindows(List<Rectangle> rectangles) { foreach (Zone driverZone in MainZone.Zones) { Bitmap zoneImage = driverZone.ZoneImage; for (int i = 1; i <= rectangles.Count; i++) { switch (i) { case 1: //First zone should be the driver's Position driverZone.AddWindow(new DriverPositionWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 2: //First zone should be the Gap to leader driverZone.AddWindow(new DriverGapToLeaderWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 3: //First zone should be the driver's Lap Time driverZone.AddWindow(new DriverLapTimeWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 4: //First zone should be the driver's DRS status driverZone.AddWindow(new DriverDrsWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 5: //First zone should be the driver's Tyre's informations driverZone.AddWindow(new DriverTyresWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 6: //First zone should be the driver's Name driverZone.AddWindow(new DriverNameWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 7: //First zone should be the driver's First Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 1, false)); break; case 8: //First zone should be the driver's Second Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 2, false)); break; case 9: //First zone should be the driver's Position Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 3, false)); break; } } } } public void AutoCalibrate() { List<Rectangle> detectedText = new List<Rectangle>(); List<Zone> zones = new List<Zone>(); TesseractEngine engine = new TesseractEngine(Window.TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Image image = MainZone.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)); } //DEBUG int i = 1; 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); //We add the driver zones Zone driverZone = new Zone(MainZone.ZoneImage, windowRectangle, \"DriverZone\"); MainZone.AddZone(driverZone); driverZone.ZoneImage.Save(\"Driver\" + i+\".png\"); i++; } } } }","title":"ConfigurationTool.cs"},{"location":"Code/ConfigurationTool.html#configurationtoolcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : ConfigurationTool.cs /// Brief : Class that contains all the methods needed to create a config file for the OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using Tesseract; using System.IO; namespace Test_Merge { public class ConfigurationTool { public Zone MainZone; public const int NUMBER_OF_DRIVERS = 20; public const int NUMBER_OF_ZONES = 9; public const string CONFIGS_FOLDER_NAME = \"./Presets/\"; public ConfigurationTool(Bitmap fullImage, Rectangle mainZoneDimensions) { MainZone = new Zone(fullImage, mainZoneDimensions,\"Main\"); AutoCalibrate(); } public void ResetMainZone() { MainZone.ResetZones(); } public void ResetWindows() { MainZone.ResetWindows(); } public void SaveToJson(List<string> drivers, string configName) { string JSON = \"\"; JSON += \"{\" + Environment.NewLine; JSON += MainZone.ToJSON() + \",\" + Environment.NewLine; JSON += \"\\\"Drivers\\\":[\" + Environment.NewLine; for (int i = 0; i < drivers.Count; i++) { JSON += \"\\\"\" + drivers[i] + \"\\\"\"; if (i < drivers.Count - 1) JSON += \",\"; JSON += Environment.NewLine; } JSON += \"]\" + Environment.NewLine; JSON += \"}\"; if (!Directory.Exists(CONFIGS_FOLDER_NAME)) Directory.CreateDirectory(CONFIGS_FOLDER_NAME); 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); } public void AddWindows(List<Rectangle> rectangles) { foreach (Zone driverZone in MainZone.Zones) { Bitmap zoneImage = driverZone.ZoneImage; for (int i = 1; i <= rectangles.Count; i++) { switch (i) { case 1: //First zone should be the driver's Position driverZone.AddWindow(new DriverPositionWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 2: //First zone should be the Gap to leader driverZone.AddWindow(new DriverGapToLeaderWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 3: //First zone should be the driver's Lap Time driverZone.AddWindow(new DriverLapTimeWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 4: //First zone should be the driver's DRS status driverZone.AddWindow(new DriverDrsWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 5: //First zone should be the driver's Tyre's informations driverZone.AddWindow(new DriverTyresWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 6: //First zone should be the driver's Name driverZone.AddWindow(new DriverNameWindow(driverZone.ZoneImage, rectangles[i - 1], false)); break; case 7: //First zone should be the driver's First Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 1, false)); break; case 8: //First zone should be the driver's Second Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 2, false)); break; case 9: //First zone should be the driver's Position Sector driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 3, false)); break; } } } } public void AutoCalibrate() { List<Rectangle> detectedText = new List<Rectangle>(); List<Zone> zones = new List<Zone>(); TesseractEngine engine = new TesseractEngine(Window.TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Image image = MainZone.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)); } //DEBUG int i = 1; 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); //We add the driver zones Zone driverZone = new Zone(MainZone.ZoneImage, windowRectangle, \"DriverZone\"); MainZone.AddZone(driverZone); driverZone.ZoneImage.Save(\"Driver\" + i+\".png\"); i++; } } } }","title":"ConfigurationTool.cs"},{"location":"Code/DriverGapToLeaderWindow.html","text":"DriverGapToLeaderWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverGapToLeaderWindow.cs /// Brief : Window containing infos about the gap to the leader of a driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { internal class DriverGapToLeaderWindow:Window { public DriverGapToLeaderWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"GapToLeader\"; } /// <summary> /// Decodes the gap to leader using Tesseract OCR /// </summary> /// <returns></returns> public override async Task<object> DecodePng() { int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Gap, Engine); return result; } } }","title":"DriverGapToLeaderWindow.cs"},{"location":"Code/DriverGapToLeaderWindow.html#drivergaptoleaderwindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverGapToLeaderWindow.cs /// Brief : Window containing infos about the gap to the leader of a driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { internal class DriverGapToLeaderWindow:Window { public DriverGapToLeaderWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"GapToLeader\"; } /// <summary> /// Decodes the gap to leader using Tesseract OCR /// </summary> /// <returns></returns> public override async Task<object> DecodePng() { int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Gap, Engine); return result; } } }","title":"DriverGapToLeaderWindow.cs"},{"location":"Code/DriverPositionWindow.html","text":"DriverPositionWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverPosition.cs /// Brief : Window containing infos about the position of a driver. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverPositionWindow:Window { public DriverPositionWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Position\"; } /// <summary> /// Decodes the position number using Tesseract OCR /// </summary> /// <returns>The position of the pilot in int</returns> public override async Task<object> DecodePng() { string ocrResult = await GetStringFromPng(WindowImage, Engine, \"0123456789\"); int position; try { position = Convert.ToInt32(ocrResult); } catch { position = -1; } return position; } } }","title":"DriverPositionWindow.cs"},{"location":"Code/DriverPositionWindow.html#driverpositionwindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverPosition.cs /// Brief : Window containing infos about the position of a driver. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverPositionWindow:Window { public DriverPositionWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Position\"; } /// <summary> /// Decodes the position number using Tesseract OCR /// </summary> /// <returns>The position of the pilot in int</returns> public override async Task<object> DecodePng() { string ocrResult = await GetStringFromPng(WindowImage, Engine, \"0123456789\"); int position; try { position = Convert.ToInt32(ocrResult); } catch { position = -1; } return position; } } }","title":"DriverPositionWindow.cs"},{"location":"Code/F1TVEmulator.html","text":"F1TVEmulator.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : F1TVEmulator.cs /// Brief : Class that contains methods to emulate a browser and navigate the F1TV website /// Version : 0.1 using OpenQA.Selenium; using OpenQA.Selenium.Firefox; using OpenQA.Selenium.Interactions; using OpenQA.Selenium.Support.UI; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Test_Merge { internal class F1TVEmulator { public const string COOKIE_HOST = \".formula1.com\"; public const string PYTHON_COOKIE_RETRIEVAL_FILENAME = \"recoverCookiesCSV.py\"; public const string GECKODRIVER_FILENAME = @\"geckodriver-v0.27.0-win64\\geckodriver.exe\"; //BE CAREFULL IF YOU CHANGE IT HERE YOU NEED TO CHANGE IT IN THE PYTHON SCRIPT TOO public const string COOKIES_CSV_FILENAME = \"cookies.csv\"; private FirefoxDriver Driver; private bool _ready; private string _grandPrixUrl; public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; } public bool Ready { get => _ready; set => _ready = value; } public F1TVEmulator(string grandPrixUrl) { GrandPrixUrl = grandPrixUrl; Ready = false; } private void StartCookieRecovering() { string scriptPath = PYTHON_COOKIE_RETRIEVAL_FILENAME; Process process = new Process(); process.StartInfo.FileName = \"python.exe\"; process.StartInfo.Arguments = scriptPath; process.StartInfo.UseShellExecute = false; process.StartInfo.RedirectStandardOutput = true; process.Start(); string output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); } public string GetCookie(string host, string name) { StartCookieRecovering(); string value = \"\"; List<Cookie> cookies = new List<Cookie>(); using (var reader = new StreamReader(COOKIES_CSV_FILENAME)) { // Read the header row and validate column order string header = reader.ReadLine(); string[] expectedColumns = { \"host_key\", \"name\", \"value\", \"path\", \"expires_utc\", \"is_secure\", \"is_httponly\" }; string[] actualColumns = header.Split(','); for (int i = 0; i < expectedColumns.Length; i++) { if (expectedColumns[i] != actualColumns[i]) { throw new InvalidOperationException($\"Expected column '{expectedColumns[i]}' at index {i} but found '{actualColumns[i]}'\"); } } // Read each data row and parse values into a Cookie object while (!reader.EndOfStream) { string line = reader.ReadLine(); string[] fields = line.Split(','); string hostname = fields[0]; string cookieName = fields[1]; if (hostname == host && cookieName == name) { value = fields[2]; } } } return value; } public async Task<int> Start() { Ready = false; string loginCookieName = \"login\"; string loginSessionCookieName = \"login-session\"; string loginCookieValue = GetCookie(COOKIE_HOST, loginCookieName); string loginSessionValue = GetCookie(COOKIE_HOST, loginSessionCookieName); int windowWidth = 1920; int windowHeight = 768; var service = FirefoxDriverService.CreateDefaultService(GECKODRIVER_FILENAME); service.Host = \"127.0.0.1\"; service.Port = 5555; FirefoxProfile profile = new FirefoxProfile(); FirefoxOptions options = new FirefoxOptions(); //profile.SetPreference(\"full-screen-api.ignore-widgets\", true); //profile.SetPreference(\"media.hardware-video-decoding.enabled\", true); //profile.SetPreference(\"full-screen-api.enabled\", true); options.Profile = profile; profile.SetPreference(\"layout.css.devPixelsPerPx\", \"1.0\"); options.AcceptInsecureCertificates = true; options.AddArgument(\"--headless\"); //options.AddArgument(\"--start-maximized\"); //options.AddArgument(\"--window-size=1920x1080\"); //options.AddArgument(\"--width=\" + windowWidth); //options.AddArgument(\"--height=\" + windowHeight); //options.AddArgument(\"-window-size=1920x1080\"); //options.AddArgument(\"--width=1920\"); //options.AddArgument(\"--height=1080\"); //profile try { Driver = new FirefoxDriver(service, options); } catch { Ready = false; return 101; } Actions actions = new Actions(Driver); var loginCookie = new Cookie(loginCookieName, loginCookieValue, COOKIE_HOST, \"/\", DateTime.Now.AddDays(5)); var loginSessionCookie = new Cookie(loginSessionCookieName, loginSessionValue, COOKIE_HOST, \"/\", DateTime.Now.AddDays(5)); Driver.Navigate().GoToUrl(\"https://f1tv.formula1.com/\"); Driver.Manage().Cookies.AddCookie(loginCookie); Driver.Manage().Cookies.AddCookie(loginSessionCookie); try { Driver.Navigate().GoToUrl(GrandPrixUrl); } catch { //The url is not a valid url Driver.Dispose(); return 103; } //Waits for the page to fully load Driver.Manage().Timeouts().PageLoad = TimeSpan.FromSeconds(30); //Removes the cookie prompt try { IWebElement conscentButton = Driver.FindElement(By.Id(\"truste-consent-button\")); conscentButton.Click(); } catch { //Could not locate the cookie button Screenshot(\"ERROR104\"); Driver.Dispose(); return 104; } //Again waits for the page to fully load (when you accept cookies it takes a little time for the page to load) //Cannot use The timeout because the feed loading is not really loading so there is not event or anything Thread.Sleep(5000); //Switches to the Data channel try { IWebElement dataChannelButton = Driver.FindElement(By.ClassName(\"data-button\")); dataChannelButton.Click(); } catch { //If the data button does not exists its because the user is not connected Screenshot(\"ERROR102\"); Driver.Dispose(); return 102; } //Open settings // Press the space key, this should make the setting button visible // It does not matter if the feed is paused because when changing channel it autoplays actions.SendKeys(OpenQA.Selenium.Keys.Space).Perform(); //Clicks on the settings Icon int tries = 0; bool success = false; while (tries < 100 && !success) { Thread.Sleep(100); try { 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(); success = true; } catch { //Sometimes it can crash because it could not get the options to show up in time. When it happens just retry success = false; tries++; } } if (!success) { Screenshot(\"ERROR105\"); Driver.Dispose(); return 105; } Screenshot(\"BEFOREFULLSCREEN\"); //Makes the feed fullscreen //Driver.Manage().Window.Size = new System.Drawing.Size(windowWidth, windowHeight); Driver.Manage().Window.Maximize(); WebDriverWait wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); try { IWebElement fullScreenButton = Driver.FindElement(By.ClassName(\"bmpui-ui-fullscreentogglebutton\")); fullScreenButton.Click(); } catch { Screenshot(\"ERROR106\"); Driver.Dispose(); return 106; } Screenshot(\"AFTERFULLSCREEN\"); //STARTUP FINISHED READY TO SCREENSHOT Ready = true; return 0; } public Bitmap Screenshot(string name = \"TEST\") { Bitmap result = new Bitmap(4242, 6969); try { //Screenshot scrsht = ((ITakesScreenshot)Driver).GetScreenshot(); //profileriver.SetPreference(\"layout.css.devPixelsPerPx\", \"1.0\"); //Screenshot scrsht = Driver.GetFullPageScreenshot(); Screenshot scrsht = Driver.GetScreenshot(); byte[] screenshotBytes = Convert.FromBase64String(scrsht.AsBase64EncodedString); MemoryStream stream = new MemoryStream(screenshotBytes); result = new Bitmap(stream); //result.Save(name + \".png\"); scrsht.SaveAsFile(name + \".png\"); } catch { //Nothing for now } return result; } public void Stop() { Ready = false; Driver.Dispose(); } public void ResetDriver() { Ready = false; Driver.Dispose(); Driver = null; } } }","title":"F1TVEmulator.cs"},{"location":"Code/F1TVEmulator.html#f1tvemulatorcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : F1TVEmulator.cs /// Brief : Class that contains methods to emulate a browser and navigate the F1TV website /// Version : 0.1 using OpenQA.Selenium; using OpenQA.Selenium.Firefox; using OpenQA.Selenium.Interactions; using OpenQA.Selenium.Support.UI; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Test_Merge { internal class F1TVEmulator { public const string COOKIE_HOST = \".formula1.com\"; public const string PYTHON_COOKIE_RETRIEVAL_FILENAME = \"recoverCookiesCSV.py\"; public const string GECKODRIVER_FILENAME = @\"geckodriver-v0.27.0-win64\\geckodriver.exe\"; //BE CAREFULL IF YOU CHANGE IT HERE YOU NEED TO CHANGE IT IN THE PYTHON SCRIPT TOO public const string COOKIES_CSV_FILENAME = \"cookies.csv\"; private FirefoxDriver Driver; private bool _ready; private string _grandPrixUrl; public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; } public bool Ready { get => _ready; set => _ready = value; } public F1TVEmulator(string grandPrixUrl) { GrandPrixUrl = grandPrixUrl; Ready = false; } private void StartCookieRecovering() { string scriptPath = PYTHON_COOKIE_RETRIEVAL_FILENAME; Process process = new Process(); process.StartInfo.FileName = \"python.exe\"; process.StartInfo.Arguments = scriptPath; process.StartInfo.UseShellExecute = false; process.StartInfo.RedirectStandardOutput = true; process.Start(); string output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); } public string GetCookie(string host, string name) { StartCookieRecovering(); string value = \"\"; List<Cookie> cookies = new List<Cookie>(); using (var reader = new StreamReader(COOKIES_CSV_FILENAME)) { // Read the header row and validate column order string header = reader.ReadLine(); string[] expectedColumns = { \"host_key\", \"name\", \"value\", \"path\", \"expires_utc\", \"is_secure\", \"is_httponly\" }; string[] actualColumns = header.Split(','); for (int i = 0; i < expectedColumns.Length; i++) { if (expectedColumns[i] != actualColumns[i]) { throw new InvalidOperationException($\"Expected column '{expectedColumns[i]}' at index {i} but found '{actualColumns[i]}'\"); } } // Read each data row and parse values into a Cookie object while (!reader.EndOfStream) { string line = reader.ReadLine(); string[] fields = line.Split(','); string hostname = fields[0]; string cookieName = fields[1]; if (hostname == host && cookieName == name) { value = fields[2]; } } } return value; } public async Task<int> Start() { Ready = false; string loginCookieName = \"login\"; string loginSessionCookieName = \"login-session\"; string loginCookieValue = GetCookie(COOKIE_HOST, loginCookieName); string loginSessionValue = GetCookie(COOKIE_HOST, loginSessionCookieName); int windowWidth = 1920; int windowHeight = 768; var service = FirefoxDriverService.CreateDefaultService(GECKODRIVER_FILENAME); service.Host = \"127.0.0.1\"; service.Port = 5555; FirefoxProfile profile = new FirefoxProfile(); FirefoxOptions options = new FirefoxOptions(); //profile.SetPreference(\"full-screen-api.ignore-widgets\", true); //profile.SetPreference(\"media.hardware-video-decoding.enabled\", true); //profile.SetPreference(\"full-screen-api.enabled\", true); options.Profile = profile; profile.SetPreference(\"layout.css.devPixelsPerPx\", \"1.0\"); options.AcceptInsecureCertificates = true; options.AddArgument(\"--headless\"); //options.AddArgument(\"--start-maximized\"); //options.AddArgument(\"--window-size=1920x1080\"); //options.AddArgument(\"--width=\" + windowWidth); //options.AddArgument(\"--height=\" + windowHeight); //options.AddArgument(\"-window-size=1920x1080\"); //options.AddArgument(\"--width=1920\"); //options.AddArgument(\"--height=1080\"); //profile try { Driver = new FirefoxDriver(service, options); } catch { Ready = false; return 101; } Actions actions = new Actions(Driver); var loginCookie = new Cookie(loginCookieName, loginCookieValue, COOKIE_HOST, \"/\", DateTime.Now.AddDays(5)); var loginSessionCookie = new Cookie(loginSessionCookieName, loginSessionValue, COOKIE_HOST, \"/\", DateTime.Now.AddDays(5)); Driver.Navigate().GoToUrl(\"https://f1tv.formula1.com/\"); Driver.Manage().Cookies.AddCookie(loginCookie); Driver.Manage().Cookies.AddCookie(loginSessionCookie); try { Driver.Navigate().GoToUrl(GrandPrixUrl); } catch { //The url is not a valid url Driver.Dispose(); return 103; } //Waits for the page to fully load Driver.Manage().Timeouts().PageLoad = TimeSpan.FromSeconds(30); //Removes the cookie prompt try { IWebElement conscentButton = Driver.FindElement(By.Id(\"truste-consent-button\")); conscentButton.Click(); } catch { //Could not locate the cookie button Screenshot(\"ERROR104\"); Driver.Dispose(); return 104; } //Again waits for the page to fully load (when you accept cookies it takes a little time for the page to load) //Cannot use The timeout because the feed loading is not really loading so there is not event or anything Thread.Sleep(5000); //Switches to the Data channel try { IWebElement dataChannelButton = Driver.FindElement(By.ClassName(\"data-button\")); dataChannelButton.Click(); } catch { //If the data button does not exists its because the user is not connected Screenshot(\"ERROR102\"); Driver.Dispose(); return 102; } //Open settings // Press the space key, this should make the setting button visible // It does not matter if the feed is paused because when changing channel it autoplays actions.SendKeys(OpenQA.Selenium.Keys.Space).Perform(); //Clicks on the settings Icon int tries = 0; bool success = false; while (tries < 100 && !success) { Thread.Sleep(100); try { 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(); success = true; } catch { //Sometimes it can crash because it could not get the options to show up in time. When it happens just retry success = false; tries++; } } if (!success) { Screenshot(\"ERROR105\"); Driver.Dispose(); return 105; } Screenshot(\"BEFOREFULLSCREEN\"); //Makes the feed fullscreen //Driver.Manage().Window.Size = new System.Drawing.Size(windowWidth, windowHeight); Driver.Manage().Window.Maximize(); WebDriverWait wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); try { IWebElement fullScreenButton = Driver.FindElement(By.ClassName(\"bmpui-ui-fullscreentogglebutton\")); fullScreenButton.Click(); } catch { Screenshot(\"ERROR106\"); Driver.Dispose(); return 106; } Screenshot(\"AFTERFULLSCREEN\"); //STARTUP FINISHED READY TO SCREENSHOT Ready = true; return 0; } public Bitmap Screenshot(string name = \"TEST\") { Bitmap result = new Bitmap(4242, 6969); try { //Screenshot scrsht = ((ITakesScreenshot)Driver).GetScreenshot(); //profileriver.SetPreference(\"layout.css.devPixelsPerPx\", \"1.0\"); //Screenshot scrsht = Driver.GetFullPageScreenshot(); Screenshot scrsht = Driver.GetScreenshot(); byte[] screenshotBytes = Convert.FromBase64String(scrsht.AsBase64EncodedString); MemoryStream stream = new MemoryStream(screenshotBytes); result = new Bitmap(stream); //result.Save(name + \".png\"); scrsht.SaveAsFile(name + \".png\"); } catch { //Nothing for now } return result; } public void Stop() { Ready = false; Driver.Dispose(); } public void ResetDriver() { Ready = false; Driver.Dispose(); Driver = null; } } }","title":"F1TVEmulator.cs"},{"location":"Code/Program.html","text":"Program.cs \ufeffusing System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; namespace Test_Merge { internal static class Program { /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } }","title":"Program.cs"},{"location":"Code/Program.html#programcs","text":"\ufeffusing System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; namespace Test_Merge { internal static class Program { /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } }","title":"Program.cs"},{"location":"Code/Window.html","text":"Window.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : Window.cs /// Brief : Default Window object that is mainly expected to be inherited. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; using System.IO; using Tesseract; using System.Text.RegularExpressions; using System.Drawing.Drawing2D; namespace Test_Merge { public class Window { private Rectangle _bounds; private Bitmap _image; private string _name; protected TesseractEngine Engine; public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Bitmap Image { get => _image; set => _image = value; } public string Name { get => _name; protected set => _name = value; } //This will have to be changed if you want to make it run on your machine public static DirectoryInfo TESS_DATA_FOLDER = new DirectoryInfo(@\"C:\\Users\\Moi\\Pictures\\SeleniumScreens\\TessData\"); public Bitmap WindowImage { get { //This little trickery lets you have the image that the window sees Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Window(Bitmap image, Rectangle bounds, bool generateEngine = true) { Image = image; Bounds = bounds; if (generateEngine) { Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Engine.DefaultPageSegMode = PageSegMode.SingleLine; } } /// <summary> /// Method that will have to be used by the childrens to let the model make them decode the images they have /// </summary> /// <returns>Returns an object because we dont know what kind of return it will be</returns> public virtual async Task<Object> DecodePng() { return \"NaN\"; } /// <summary> /// Method that will have to be used by the childrens to let the model make them decode the images they have /// </summary> /// <param name=\"driverList\">This is a list of the different possible drivers in the race. It should not be too big but NEVER be too short</param> /// <returns>Returns an object because we dont know what kind of return it will be</returns> public virtual async Task<Object> DecodePng(List<string> driverList) { return \"NaN\"; } /// <summary> /// This converts an image into a byte[]. It can be usefull when doing unsafe stuff. Use at your own risks /// </summary> /// <param name=\"inputImage\">The image you want to convert</param> /// <returns>A byte array containing the image informations</returns> public static byte[] ImageToByte(Image inputImage) { using (var stream = new MemoryStream()) { inputImage.Save(stream, System.Drawing.Imaging.ImageFormat.Png); return stream.ToArray(); } } /// <summary> /// This method is used to recover a time from a PNG using Tesseract OCR /// </summary> /// <param name=\"windowImage\">The image where the text is</param> /// <param name=\"windowType\">The type of window it is</param> /// <param name=\"Engine\">The Tesseract Engine</param> /// <returns>The time in milliseconds</returns> public static async Task<int> GetTimeFromPng(Bitmap windowImage, OcrImage.WindowType windowType, TesseractEngine Engine) { //Kind of a big method but it has a lot of error handling and has to work with three special cases string rawResult = \"\"; int result = 0; switch (windowType) { case OcrImage.WindowType.Sector: //The usual sector is in this form : 33.456 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.\"); break; case OcrImage.WindowType.LapTime: //The usual Lap time is in this form : 1:45:345 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.:\"); break; case OcrImage.WindowType.Gap: //The usual Gap is in this form : + 34.567 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.+\"); break; default: Engine.SetVariable(\"tessedit_char_whitelist\", \"\"); break; } Bitmap enhancedImage = new OcrImage(windowImage).Enhance(windowType); var tessImage = Pix.LoadFromMemory(ImageToByte(enhancedImage)); Page page = Engine.Process(tessImage); Graphics g = Graphics.FromImage(enhancedImage); // Get the iterator for the page layout using (var iter = page.GetIterator()) { // Loop over the elements of the page layout iter.Begin(); do { // Get the text for the current element try { rawResult += iter.GetText(PageIteratorLevel.Word); } catch { //nothing we just dont add it if its not a number } } while (iter.Next(PageIteratorLevel.Word)); } List<string> rawNumbers; //In the gaps we can find '+' but we dont care about it its redondant a driver will never be - something if (windowType == OcrImage.WindowType.Gap) rawResult = Regex.Replace(rawResult, \"[^0-9.:]\", \"\"); //Splits into minuts seconds miliseconds rawNumbers = rawResult.Split('.', ':').ToList<string>(); //removes any empty cells (tho this usually sign of a really bad OCR implementation tbh will have to be fixed higher in the chian) rawNumbers.RemoveAll(x => ((string)x) == \"\"); if (rawNumbers.Count == 3) { //mm:ss:ms result = (Convert.ToInt32(rawNumbers[0]) * 1000 * 60) + (Convert.ToInt32(rawNumbers[1]) * 1000) + Convert.ToInt32(rawNumbers[2]); } else { if (rawNumbers.Count == 2) { //ss:ms result = (Convert.ToInt32(rawNumbers[0]) * 1000) + Convert.ToInt32(rawNumbers[1]); if (result > 999999) { //We know that we have way too much seconds to make a minut //Its usually because the \":\" have been interpreted as a number int minuts = (int)(rawNumbers[0][0] - '0'); // rawNumbers[0][1] should contain the : that has been mistaken int seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString()); int ms = Convert.ToInt32(rawNumbers[1]); result = (Convert.ToInt32(minuts) * 1000 * 60) + (Convert.ToInt32(seconds) * 1000) + Convert.ToInt32(ms); } } else { if (rawNumbers.Count == 1) { try { result = Convert.ToInt32(rawNumbers[0]); } catch { //It can be because the input is empty or because its the LEADER bracket result = 0; } } else { //Auuuugh result = 0; } } } page.Dispose(); return result; } /// <summary> /// Method that recovers strings from an image using Tesseract OCR /// </summary> /// <param name=\"WindowImage\">The image of the window that contains text</param> /// <param name=\"Engine\">The Tesseract engine</param> /// <param name=\"allowedChars\">The list of allowed chars</param> /// <param name=\"windowType\">The type of window the text is on. Depending on the context the OCR will behave differently</param> /// <returns>the string it found</returns> public static async Task<string> GetStringFromPng(Bitmap WindowImage, TesseractEngine Engine, string allowedChars = \"\", OcrImage.WindowType windowType = OcrImage.WindowType.Text) { string result = \"\"; Engine.SetVariable(\"tessedit_char_whitelist\", allowedChars); Bitmap rawData = WindowImage; Bitmap enhancedImage = new OcrImage(rawData).Enhance(windowType); Page page = Engine.Process(enhancedImage); using (var iter = page.GetIterator()) { iter.Begin(); do { result += iter.GetText(PageIteratorLevel.Word); } while (iter.Next(PageIteratorLevel.Word)); } page.Dispose(); return result; } /// <summary> /// Get a smaller image from a bigger one /// </summary> /// <param name=\"inputBitmap\">The big bitmap you want to get a part of</param> /// <param name=\"newBitmapDimensions\">The dimensions of the new bitmap</param> /// <returns>The little bitmap</returns> protected Bitmap GetSmallBitmapFromBigOne(Bitmap inputBitmap, Rectangle newBitmapDimensions) { Bitmap sample = new Bitmap(newBitmapDimensions.Width, newBitmapDimensions.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(inputBitmap, new Rectangle(0, 0, sample.Width, sample.Height), newBitmapDimensions, GraphicsUnit.Pixel); return sample; } /// <summary> /// Returns the closest string from a list of options /// </summary> /// <param name=\"options\">an array of all the possibilities</param> /// <param name=\"testString\">the string you want to compare</param> /// <returns>The closest option</returns> protected static string FindClosestMatch(List<string> options, string testString) { var closestMatch = \"\"; var closestDistance = int.MaxValue; foreach (var item in options) { var distance = LevenshteinDistance(item, testString); if (distance < closestDistance) { closestMatch = item; closestDistance = distance; } } return closestMatch; } //This method has been generated with the help of ChatGPT /// <summary> /// Method that computes a score of distance between two strings /// </summary> /// <param name=\"string1\">The first string (order irrelevant)</param> /// <param name=\"string2\">The second string (order irrelevant)</param> /// <returns>The levenshtein distance</returns> protected static int LevenshteinDistance(string string1, string string2) { if (string.IsNullOrEmpty(string1)) { return string.IsNullOrEmpty(string2) ? 0 : string2.Length; } if (string.IsNullOrEmpty(string2)) { return string.IsNullOrEmpty(string1) ? 0 : string1.Length; } var d = new int[string1.Length + 1, string2.Length + 1]; for (var i = 0; i <= string1.Length; i++) { d[i, 0] = i; } for (var j = 0; j <= string2.Length; j++) { d[0, j] = j; } for (var i = 1; i <= string1.Length; i++) { for (var j = 1; j <= string2.Length; j++) { var cost = (string1[i - 1] == string2[j - 1]) ? 0 : 1; d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost); } } return d[string1.Length, string2.Length]; } public virtual string ToJSON() { string result = \"\"; result += \"\\\"\" + Name + \"\\\"\" + \":{\" + Environment.NewLine; result += \"\\t\" + \"\\\"x\\\":\" + Bounds.X + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"y\\\":\" + Bounds.Y + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"width\\\":\" + Bounds.Width + Environment.NewLine; result += \"}\"; return result; } } }","title":"Window.cs"},{"location":"Code/Window.html#windowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : Window.cs /// Brief : Default Window object that is mainly expected to be inherited. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; using System.IO; using Tesseract; using System.Text.RegularExpressions; using System.Drawing.Drawing2D; namespace Test_Merge { public class Window { private Rectangle _bounds; private Bitmap _image; private string _name; protected TesseractEngine Engine; public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Bitmap Image { get => _image; set => _image = value; } public string Name { get => _name; protected set => _name = value; } //This will have to be changed if you want to make it run on your machine public static DirectoryInfo TESS_DATA_FOLDER = new DirectoryInfo(@\"C:\\Users\\Moi\\Pictures\\SeleniumScreens\\TessData\"); public Bitmap WindowImage { get { //This little trickery lets you have the image that the window sees Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Window(Bitmap image, Rectangle bounds, bool generateEngine = true) { Image = image; Bounds = bounds; if (generateEngine) { Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, \"eng\", EngineMode.Default); Engine.DefaultPageSegMode = PageSegMode.SingleLine; } } /// <summary> /// Method that will have to be used by the childrens to let the model make them decode the images they have /// </summary> /// <returns>Returns an object because we dont know what kind of return it will be</returns> public virtual async Task<Object> DecodePng() { return \"NaN\"; } /// <summary> /// Method that will have to be used by the childrens to let the model make them decode the images they have /// </summary> /// <param name=\"driverList\">This is a list of the different possible drivers in the race. It should not be too big but NEVER be too short</param> /// <returns>Returns an object because we dont know what kind of return it will be</returns> public virtual async Task<Object> DecodePng(List<string> driverList) { return \"NaN\"; } /// <summary> /// This converts an image into a byte[]. It can be usefull when doing unsafe stuff. Use at your own risks /// </summary> /// <param name=\"inputImage\">The image you want to convert</param> /// <returns>A byte array containing the image informations</returns> public static byte[] ImageToByte(Image inputImage) { using (var stream = new MemoryStream()) { inputImage.Save(stream, System.Drawing.Imaging.ImageFormat.Png); return stream.ToArray(); } } /// <summary> /// This method is used to recover a time from a PNG using Tesseract OCR /// </summary> /// <param name=\"windowImage\">The image where the text is</param> /// <param name=\"windowType\">The type of window it is</param> /// <param name=\"Engine\">The Tesseract Engine</param> /// <returns>The time in milliseconds</returns> public static async Task<int> GetTimeFromPng(Bitmap windowImage, OcrImage.WindowType windowType, TesseractEngine Engine) { //Kind of a big method but it has a lot of error handling and has to work with three special cases string rawResult = \"\"; int result = 0; switch (windowType) { case OcrImage.WindowType.Sector: //The usual sector is in this form : 33.456 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.\"); break; case OcrImage.WindowType.LapTime: //The usual Lap time is in this form : 1:45:345 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.:\"); break; case OcrImage.WindowType.Gap: //The usual Gap is in this form : + 34.567 Engine.SetVariable(\"tessedit_char_whitelist\", \"0123456789.+\"); break; default: Engine.SetVariable(\"tessedit_char_whitelist\", \"\"); break; } Bitmap enhancedImage = new OcrImage(windowImage).Enhance(windowType); var tessImage = Pix.LoadFromMemory(ImageToByte(enhancedImage)); Page page = Engine.Process(tessImage); Graphics g = Graphics.FromImage(enhancedImage); // Get the iterator for the page layout using (var iter = page.GetIterator()) { // Loop over the elements of the page layout iter.Begin(); do { // Get the text for the current element try { rawResult += iter.GetText(PageIteratorLevel.Word); } catch { //nothing we just dont add it if its not a number } } while (iter.Next(PageIteratorLevel.Word)); } List<string> rawNumbers; //In the gaps we can find '+' but we dont care about it its redondant a driver will never be - something if (windowType == OcrImage.WindowType.Gap) rawResult = Regex.Replace(rawResult, \"[^0-9.:]\", \"\"); //Splits into minuts seconds miliseconds rawNumbers = rawResult.Split('.', ':').ToList<string>(); //removes any empty cells (tho this usually sign of a really bad OCR implementation tbh will have to be fixed higher in the chian) rawNumbers.RemoveAll(x => ((string)x) == \"\"); if (rawNumbers.Count == 3) { //mm:ss:ms result = (Convert.ToInt32(rawNumbers[0]) * 1000 * 60) + (Convert.ToInt32(rawNumbers[1]) * 1000) + Convert.ToInt32(rawNumbers[2]); } else { if (rawNumbers.Count == 2) { //ss:ms result = (Convert.ToInt32(rawNumbers[0]) * 1000) + Convert.ToInt32(rawNumbers[1]); if (result > 999999) { //We know that we have way too much seconds to make a minut //Its usually because the \":\" have been interpreted as a number int minuts = (int)(rawNumbers[0][0] - '0'); // rawNumbers[0][1] should contain the : that has been mistaken int seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString()); int ms = Convert.ToInt32(rawNumbers[1]); result = (Convert.ToInt32(minuts) * 1000 * 60) + (Convert.ToInt32(seconds) * 1000) + Convert.ToInt32(ms); } } else { if (rawNumbers.Count == 1) { try { result = Convert.ToInt32(rawNumbers[0]); } catch { //It can be because the input is empty or because its the LEADER bracket result = 0; } } else { //Auuuugh result = 0; } } } page.Dispose(); return result; } /// <summary> /// Method that recovers strings from an image using Tesseract OCR /// </summary> /// <param name=\"WindowImage\">The image of the window that contains text</param> /// <param name=\"Engine\">The Tesseract engine</param> /// <param name=\"allowedChars\">The list of allowed chars</param> /// <param name=\"windowType\">The type of window the text is on. Depending on the context the OCR will behave differently</param> /// <returns>the string it found</returns> public static async Task<string> GetStringFromPng(Bitmap WindowImage, TesseractEngine Engine, string allowedChars = \"\", OcrImage.WindowType windowType = OcrImage.WindowType.Text) { string result = \"\"; Engine.SetVariable(\"tessedit_char_whitelist\", allowedChars); Bitmap rawData = WindowImage; Bitmap enhancedImage = new OcrImage(rawData).Enhance(windowType); Page page = Engine.Process(enhancedImage); using (var iter = page.GetIterator()) { iter.Begin(); do { result += iter.GetText(PageIteratorLevel.Word); } while (iter.Next(PageIteratorLevel.Word)); } page.Dispose(); return result; } /// <summary> /// Get a smaller image from a bigger one /// </summary> /// <param name=\"inputBitmap\">The big bitmap you want to get a part of</param> /// <param name=\"newBitmapDimensions\">The dimensions of the new bitmap</param> /// <returns>The little bitmap</returns> protected Bitmap GetSmallBitmapFromBigOne(Bitmap inputBitmap, Rectangle newBitmapDimensions) { Bitmap sample = new Bitmap(newBitmapDimensions.Width, newBitmapDimensions.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(inputBitmap, new Rectangle(0, 0, sample.Width, sample.Height), newBitmapDimensions, GraphicsUnit.Pixel); return sample; } /// <summary> /// Returns the closest string from a list of options /// </summary> /// <param name=\"options\">an array of all the possibilities</param> /// <param name=\"testString\">the string you want to compare</param> /// <returns>The closest option</returns> protected static string FindClosestMatch(List<string> options, string testString) { var closestMatch = \"\"; var closestDistance = int.MaxValue; foreach (var item in options) { var distance = LevenshteinDistance(item, testString); if (distance < closestDistance) { closestMatch = item; closestDistance = distance; } } return closestMatch; } //This method has been generated with the help of ChatGPT /// <summary> /// Method that computes a score of distance between two strings /// </summary> /// <param name=\"string1\">The first string (order irrelevant)</param> /// <param name=\"string2\">The second string (order irrelevant)</param> /// <returns>The levenshtein distance</returns> protected static int LevenshteinDistance(string string1, string string2) { if (string.IsNullOrEmpty(string1)) { return string.IsNullOrEmpty(string2) ? 0 : string2.Length; } if (string.IsNullOrEmpty(string2)) { return string.IsNullOrEmpty(string1) ? 0 : string1.Length; } var d = new int[string1.Length + 1, string2.Length + 1]; for (var i = 0; i <= string1.Length; i++) { d[i, 0] = i; } for (var j = 0; j <= string2.Length; j++) { d[0, j] = j; } for (var i = 1; i <= string1.Length; i++) { for (var j = 1; j <= string2.Length; j++) { var cost = (string1[i - 1] == string2[j - 1]) ? 0 : 1; d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost); } } return d[string1.Length, string2.Length]; } public virtual string ToJSON() { string result = \"\"; result += \"\\\"\" + Name + \"\\\"\" + \":{\" + Environment.NewLine; result += \"\\t\" + \"\\\"x\\\":\" + Bounds.X + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"y\\\":\" + Bounds.Y + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"width\\\":\" + Bounds.Width + Environment.NewLine; result += \"}\"; return result; } } }","title":"Window.cs"},{"location":"Code/DriverData.html","text":"DriverData.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverData.cs /// Brief : Class used to store Driver informations /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { public class DriverData { public bool DRS; //True = Drs is opened public int GapToLeader; //In ms public int LapTime; //In ms public string Name; //Ex: LECLERC public int Position; //Ex: 1 public int Sector1; //in ms public int Sector2; //in ms public int Sector3; //in ms public Tyre CurrentTyre;//Ex Soft 11 laps public DriverData(bool dRS, int gapToLeader, int lapTime, string name, int position, int sector1, int sector2, int sector3, Tyre tyre) { DRS = dRS; GapToLeader = gapToLeader; LapTime = lapTime; Name = name; Position = position; Sector1 = sector1; Sector2 = sector2; Sector3 = sector3; CurrentTyre = tyre; } public DriverData() { DRS = false; GapToLeader = -1; LapTime = -1; Name = \"Unknown\"; Position = -1; Sector1 = -1; Sector2 = -1; Sector3 = -1; CurrentTyre = new Tyre(Tyre.Type.Undefined, -1); } /// <summary> /// Method that displays all the data found in a string /// </summary> /// <returns>string containing all the driver datas</returns> public override string ToString() { string result = \"\"; //Position result += \"Position : \" + Position + Environment.NewLine; //Gap result += \"GapToLeader : \" + Reader.ConvertMsToTime(GapToLeader) + Environment.NewLine; //LapTime result += \"LapTime : \" + Reader.ConvertMsToTime(LapTime) + Environment.NewLine; //DRS result += \"DRS : \" + DRS + Environment.NewLine; //Tyres result += \"Uses \" + CurrentTyre.Coumpound + \" tyre \" + CurrentTyre.NumberOfLaps + \" laps old\" + Environment.NewLine; //Name result += \"DriverName : \" + Name + Environment.NewLine; //Sector 1 result += \"Sector1 : \" + Reader.ConvertMsToTime(Sector1) + Environment.NewLine; //Sector 1 result += \"Sector2 : \" + Reader.ConvertMsToTime(Sector2) + Environment.NewLine; //Sector 1 result += \"Sector3 : \" + Reader.ConvertMsToTime(Sector3) + Environment.NewLine; return result; } } //Structure to store tyres infos public struct Tyre { //If new tyres were to be added you will have to need to change this enum public enum Type { Soft, Medium, Hard, Inter, Wet, Undefined } public Type Coumpound; public int NumberOfLaps; public Tyre(Type type, int laps) { Coumpound = type; NumberOfLaps = laps; } } }","title":"DriverData.cs"},{"location":"Code/DriverData.html#driverdatacs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverData.cs /// Brief : Class used to store Driver informations /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { public class DriverData { public bool DRS; //True = Drs is opened public int GapToLeader; //In ms public int LapTime; //In ms public string Name; //Ex: LECLERC public int Position; //Ex: 1 public int Sector1; //in ms public int Sector2; //in ms public int Sector3; //in ms public Tyre CurrentTyre;//Ex Soft 11 laps public DriverData(bool dRS, int gapToLeader, int lapTime, string name, int position, int sector1, int sector2, int sector3, Tyre tyre) { DRS = dRS; GapToLeader = gapToLeader; LapTime = lapTime; Name = name; Position = position; Sector1 = sector1; Sector2 = sector2; Sector3 = sector3; CurrentTyre = tyre; } public DriverData() { DRS = false; GapToLeader = -1; LapTime = -1; Name = \"Unknown\"; Position = -1; Sector1 = -1; Sector2 = -1; Sector3 = -1; CurrentTyre = new Tyre(Tyre.Type.Undefined, -1); } /// <summary> /// Method that displays all the data found in a string /// </summary> /// <returns>string containing all the driver datas</returns> public override string ToString() { string result = \"\"; //Position result += \"Position : \" + Position + Environment.NewLine; //Gap result += \"GapToLeader : \" + Reader.ConvertMsToTime(GapToLeader) + Environment.NewLine; //LapTime result += \"LapTime : \" + Reader.ConvertMsToTime(LapTime) + Environment.NewLine; //DRS result += \"DRS : \" + DRS + Environment.NewLine; //Tyres result += \"Uses \" + CurrentTyre.Coumpound + \" tyre \" + CurrentTyre.NumberOfLaps + \" laps old\" + Environment.NewLine; //Name result += \"DriverName : \" + Name + Environment.NewLine; //Sector 1 result += \"Sector1 : \" + Reader.ConvertMsToTime(Sector1) + Environment.NewLine; //Sector 1 result += \"Sector2 : \" + Reader.ConvertMsToTime(Sector2) + Environment.NewLine; //Sector 1 result += \"Sector3 : \" + Reader.ConvertMsToTime(Sector3) + Environment.NewLine; return result; } } //Structure to store tyres infos public struct Tyre { //If new tyres were to be added you will have to need to change this enum public enum Type { Soft, Medium, Hard, Inter, Wet, Undefined } public Type Coumpound; public int NumberOfLaps; public Tyre(Type type, int laps) { Coumpound = type; NumberOfLaps = laps; } } }","title":"DriverData.cs"},{"location":"Code/DriverLapTimeWindow.html","text":"DriverLapTimeWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverLapTimeWindow /// Brief : Window containing infos about the lap time of a driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { internal class DriverLapTimeWindow:Window { public DriverLapTimeWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"LapTime\"; } /// <summary> /// Decodes the lap time contained in the image using OCR Tesseract /// </summary> /// <returns>The laptime in int (ms)</returns> public override async Task<object> DecodePng() { int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.LapTime, Engine); return result; } } }","title":"DriverLapTimeWindow.cs"},{"location":"Code/DriverLapTimeWindow.html#driverlaptimewindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverLapTimeWindow /// Brief : Window containing infos about the lap time of a driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { internal class DriverLapTimeWindow:Window { public DriverLapTimeWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"LapTime\"; } /// <summary> /// Decodes the lap time contained in the image using OCR Tesseract /// </summary> /// <returns>The laptime in int (ms)</returns> public override async Task<object> DecodePng() { int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.LapTime, Engine); return result; } } }","title":"DriverLapTimeWindow.cs"},{"location":"Code/DriverSectorWindow.html","text":"DriverSectorWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverSectorWindow.cs /// Brief : Window containing infos about a driver sector time. Can be the first second or third, does not matter. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { internal class DriverSectorWindow:Window { public DriverSectorWindow(Bitmap image, Rectangle bounds, int sectorId, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Sector\"+sectorId; } /// <summary> /// Decodes the sector /// </summary> /// <returns>the sector time in int (ms)</returns> public override async Task<object> DecodePng() { int ocrResult = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector, Engine); return ocrResult; } } }","title":"DriverSectorWindow.cs"},{"location":"Code/DriverSectorWindow.html#driversectorwindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverSectorWindow.cs /// Brief : Window containing infos about a driver sector time. Can be the first second or third, does not matter. /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { internal class DriverSectorWindow:Window { public DriverSectorWindow(Bitmap image, Rectangle bounds, int sectorId, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Sector\"+sectorId; } /// <summary> /// Decodes the sector /// </summary> /// <returns>the sector time in int (ms)</returns> public override async Task<object> DecodePng() { int ocrResult = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector, Engine); return ocrResult; } } }","title":"DriverSectorWindow.cs"},{"location":"Code/Form1.html","text":"Form1.cs \ufeffusing System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace Test_Merge { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnSettings_Click(object sender, EventArgs e) { Settings settingsForm = new Settings(); settingsForm.ShowDialog(); MessageBox.Show(settingsForm.GrandPrixUrl + Environment.NewLine + settingsForm.GrandPrixName + Environment.NewLine + settingsForm.GrandPrixYear); } } }","title":"Form1.cs"},{"location":"Code/Form1.html#form1cs","text":"\ufeffusing System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace Test_Merge { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnSettings_Click(object sender, EventArgs e) { Settings settingsForm = new Settings(); settingsForm.ShowDialog(); MessageBox.Show(settingsForm.GrandPrixUrl + Environment.NewLine + settingsForm.GrandPrixName + Environment.NewLine + settingsForm.GrandPrixYear); } } }","title":"Form1.cs"},{"location":"Code/Reader.html","text":"Reader.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : Reader.cs /// Brief : Class used to Read the config file for the OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; using System.Windows.Forms; using System.IO; using System.Text.Json; namespace Test_Merge { public class Reader { const int NUMBER_OF_DRIVERS = 20; public List<string> Drivers; public List<Zone> MainZones; public Reader(string configFile, Bitmap image,bool loadOCR = true) { MainZones = Load(image,configFile,ref Drivers,loadOCR); } /// <summary> /// Method that reads the JSON config file and create all the Zones and Windows /// </summary> /// <param name=\"imageNumber\">The image #id on wich you want to create the zones on</param> public static List<Zone> Load(Bitmap image,string configFilePath,ref List<string> driverListToFill,bool LoadOCR) { List<Zone> mainZones = new List<Zone>(); Bitmap fullImage = image; List<string> drivers; Zone mainZone; try { using (var streamReader = new StreamReader(configFilePath)) { var jsonText = streamReader.ReadToEnd(); var jsonDocument = JsonDocument.Parse(jsonText); var driversNames = jsonDocument.RootElement.GetProperty(\"Drivers\"); driverListToFill = new List<string>(); foreach (var nameElement in driversNames.EnumerateArray()) { driverListToFill.Add(nameElement.GetString()); } var mainProperty = jsonDocument.RootElement.GetProperty(\"Main\"); Point MainPosition = new Point(mainProperty.GetProperty(\"x\").GetInt32(), mainProperty.GetProperty(\"y\").GetInt32()); Size MainSize = new Size(mainProperty.GetProperty(\"width\").GetInt32(), mainProperty.GetProperty(\"height\").GetInt32()); Rectangle MainRectangle = new Rectangle(MainPosition, MainSize); mainZone = new Zone(image, MainRectangle,\"Main\"); var zones = mainProperty.GetProperty(\"Zones\"); var driverZone = zones[0].GetProperty(\"DriverZone\"); Point FirstZonePosition = new Point(driverZone.GetProperty(\"x\").GetInt32(), driverZone.GetProperty(\"y\").GetInt32()); Size FirstZoneSize = new Size(driverZone.GetProperty(\"width\").GetInt32(), driverZone.GetProperty(\"height\").GetInt32()); var windows = driverZone.GetProperty(\"Windows\"); var driverPosition = windows[0].GetProperty(\"Position\"); Size driverPositionArea = new Size(driverPosition.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverPositionPosition = new Point(driverPosition.GetProperty(\"x\").GetInt32(), driverPosition.GetProperty(\"y\").GetInt32()); var driverGapToLeader = windows[0].GetProperty(\"GapToLeader\"); Size driverGapToLeaderArea = new Size(driverGapToLeader.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverGapToLeaderPosition = new Point(driverGapToLeader.GetProperty(\"x\").GetInt32(), driverGapToLeader.GetProperty(\"y\").GetInt32()); var driverLapTime = windows[0].GetProperty(\"LapTime\"); Size driverLapTimeArea = new Size(driverLapTime.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverLapTimePosition = new Point(driverLapTime.GetProperty(\"x\").GetInt32(), driverLapTime.GetProperty(\"y\").GetInt32()); var driverDrs = windows[0].GetProperty(\"DRS\"); Size driverDrsArea = new Size(driverDrs.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverDrsPosition = new Point(driverDrs.GetProperty(\"x\").GetInt32(), driverDrs.GetProperty(\"y\").GetInt32()); var driverTyres = windows[0].GetProperty(\"Tyres\"); Size driverTyresArea = new Size(driverTyres.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverTyresPosition = new Point(driverTyres.GetProperty(\"x\").GetInt32(), driverTyres.GetProperty(\"y\").GetInt32()); var driverName = windows[0].GetProperty(\"Name\"); Size driverNameArea = new Size(driverName.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverNamePosition = new Point(driverName.GetProperty(\"x\").GetInt32(), driverName.GetProperty(\"y\").GetInt32()); var driverSector1 = windows[0].GetProperty(\"Sector1\"); Size driverSector1Area = new Size(driverSector1.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector1Position = new Point(driverSector1.GetProperty(\"x\").GetInt32(), driverSector1.GetProperty(\"y\").GetInt32()); var driverSector2 = windows[0].GetProperty(\"Sector2\"); Size driverSector2Area = new Size(driverSector2.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector2Position = new Point(driverSector2.GetProperty(\"x\").GetInt32(), driverSector2.GetProperty(\"y\").GetInt32()); var driverSector3 = windows[0].GetProperty(\"Sector3\"); Size driverSector3Area = new Size(driverSector3.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector3Position = new Point(driverSector3.GetProperty(\"x\").GetInt32(), driverSector3.GetProperty(\"y\").GetInt32()); float offset = (((float)mainZone.ZoneImage.Height - (float)(driverListToFill.Count * FirstZoneSize.Height)) / (float)driverListToFill.Count); Bitmap MainZoneImage = mainZone.ZoneImage; List<Zone> zonesToAdd = new List<Zone>(); List<Bitmap> zonesImages = new List<Bitmap>(); for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset)); Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize), \"DriverZone\"); zonesToAdd.Add(newDriverZone); zonesImages.Add(newDriverZone.ZoneImage); newDriverZone.ZoneImage.Save(\"Driver\"+i+\".png\"); } //Parallel.For(0, NUMBER_OF_DRIVERS, i => for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Zone newDriverZone = zonesToAdd[(int)i]; Bitmap zoneImg = zonesImages[(int)i]; newDriverZone.AddWindow(new DriverPositionWindow(zoneImg, new Rectangle(driverPositionPosition, driverPositionArea),LoadOCR)); newDriverZone.AddWindow(new DriverGapToLeaderWindow(zoneImg, new Rectangle(driverGapToLeaderPosition, driverGapToLeaderArea), LoadOCR)); newDriverZone.AddWindow(new DriverLapTimeWindow(zoneImg, new Rectangle(driverLapTimePosition, driverLapTimeArea), LoadOCR)); newDriverZone.AddWindow(new DriverDrsWindow(zoneImg, new Rectangle(driverDrsPosition, driverDrsArea), LoadOCR)); newDriverZone.AddWindow(new DriverTyresWindow(zoneImg, new Rectangle(driverTyresPosition, driverTyresArea), LoadOCR)); newDriverZone.AddWindow(new DriverNameWindow(zoneImg, new Rectangle(driverNamePosition, driverNameArea), LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector1Position, driverSector1Area),1, LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector2Position, driverSector2Area),2, LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector3Position, driverSector3Area),3, LoadOCR)); mainZone.AddZone(newDriverZone); }//); //MessageBox.Show(\"We have a main zone with \" + MainZone.Zones.Count() + \" Driver zones with \" + MainZone.Zones[4].Windows.Count() + \" windows each and we have \" + Drivers.Count + \" drivers\"); mainZones.Add(mainZone); } } catch (IOException ex) { MessageBox.Show(\"Error reading JSON file: \" + ex.Message); } catch (JsonException ex) { MessageBox.Show(\"Invalid JSON format: \" + ex.Message); } return mainZones; } /// <summary> /// Method that calls all the zones and windows to get the content they can find on the image to display them /// </summary> /// <param name=\"idImage\">The id of the image we are working with</param> /// <returns>a string representation of all the returns</returns> public async Task<string> Decode(List<Zone> mainZones,List<string> drivers) { string result = \"\"; List<DriverData> mainResults = new List<DriverData>(); //Decode for (int mainZoneId = 0; mainZoneId < mainZones.Count; mainZoneId++) { switch (mainZoneId) { case 0: //Main Zone foreach (Zone z in mainZones[mainZoneId].Zones) { mainResults.Add(await z.Decode(Drivers)); } break; //Next there could be a Title Zone and TrackInfoZone } } //Display foreach (DriverData driver in mainResults) { result += driver.ToString(); result += Environment.NewLine; } return result; } /// <summary> /// Method that can be used to convert an amount of miliseconds into a more readable human form /// </summary> /// <param name=\"amountOfMs\">The given amount of miliseconds ton convert</param> /// <returns>A human readable string that represents the ms</returns> public static string ConvertMsToTime(int amountOfMs) { //Convert.ToInt32 would round upand I dont want that int minuts = (int)((float)amountOfMs / (1000f * 60f)); int seconds = (int)((amountOfMs - (minuts * 60f * 1000f)) / 1000); int ms = amountOfMs - ((minuts * 60 * 1000) + (seconds * 1000)); return minuts + \":\" + seconds.ToString(\"00\") + \":\" + ms.ToString(\"000\"); } /// <summary> /// Old method that can draw on an image where the windows and zones are created. mostly used for debugging /// </summary> /// <param name=\"idImage\">the #id of the image we are working with</param> /// <returns>the drawed bitmap</returns> public Bitmap Draw(Bitmap image,List<Zone> mainZones) { Graphics g = Graphics.FromImage(image); foreach (Zone z in mainZones) { int count = 0; foreach (Zone zz in z.Zones) { g.DrawRectangle(Pens.Red, z.Bounds); foreach (Window w in zz.Windows) { g.DrawRectangle(Pens.Blue, new Rectangle(z.Bounds.X + zz.Bounds.X, z.Bounds.Y + zz.Bounds.Y, zz.Bounds.Width, zz.Bounds.Height)); } count++; } } return image; } } }","title":"Reader.cs"},{"location":"Code/Reader.html#readercs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : Reader.cs /// Brief : Class used to Read the config file for the OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; using System.Windows.Forms; using System.IO; using System.Text.Json; namespace Test_Merge { public class Reader { const int NUMBER_OF_DRIVERS = 20; public List<string> Drivers; public List<Zone> MainZones; public Reader(string configFile, Bitmap image,bool loadOCR = true) { MainZones = Load(image,configFile,ref Drivers,loadOCR); } /// <summary> /// Method that reads the JSON config file and create all the Zones and Windows /// </summary> /// <param name=\"imageNumber\">The image #id on wich you want to create the zones on</param> public static List<Zone> Load(Bitmap image,string configFilePath,ref List<string> driverListToFill,bool LoadOCR) { List<Zone> mainZones = new List<Zone>(); Bitmap fullImage = image; List<string> drivers; Zone mainZone; try { using (var streamReader = new StreamReader(configFilePath)) { var jsonText = streamReader.ReadToEnd(); var jsonDocument = JsonDocument.Parse(jsonText); var driversNames = jsonDocument.RootElement.GetProperty(\"Drivers\"); driverListToFill = new List<string>(); foreach (var nameElement in driversNames.EnumerateArray()) { driverListToFill.Add(nameElement.GetString()); } var mainProperty = jsonDocument.RootElement.GetProperty(\"Main\"); Point MainPosition = new Point(mainProperty.GetProperty(\"x\").GetInt32(), mainProperty.GetProperty(\"y\").GetInt32()); Size MainSize = new Size(mainProperty.GetProperty(\"width\").GetInt32(), mainProperty.GetProperty(\"height\").GetInt32()); Rectangle MainRectangle = new Rectangle(MainPosition, MainSize); mainZone = new Zone(image, MainRectangle,\"Main\"); var zones = mainProperty.GetProperty(\"Zones\"); var driverZone = zones[0].GetProperty(\"DriverZone\"); Point FirstZonePosition = new Point(driverZone.GetProperty(\"x\").GetInt32(), driverZone.GetProperty(\"y\").GetInt32()); Size FirstZoneSize = new Size(driverZone.GetProperty(\"width\").GetInt32(), driverZone.GetProperty(\"height\").GetInt32()); var windows = driverZone.GetProperty(\"Windows\"); var driverPosition = windows[0].GetProperty(\"Position\"); Size driverPositionArea = new Size(driverPosition.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverPositionPosition = new Point(driverPosition.GetProperty(\"x\").GetInt32(), driverPosition.GetProperty(\"y\").GetInt32()); var driverGapToLeader = windows[0].GetProperty(\"GapToLeader\"); Size driverGapToLeaderArea = new Size(driverGapToLeader.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverGapToLeaderPosition = new Point(driverGapToLeader.GetProperty(\"x\").GetInt32(), driverGapToLeader.GetProperty(\"y\").GetInt32()); var driverLapTime = windows[0].GetProperty(\"LapTime\"); Size driverLapTimeArea = new Size(driverLapTime.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverLapTimePosition = new Point(driverLapTime.GetProperty(\"x\").GetInt32(), driverLapTime.GetProperty(\"y\").GetInt32()); var driverDrs = windows[0].GetProperty(\"DRS\"); Size driverDrsArea = new Size(driverDrs.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverDrsPosition = new Point(driverDrs.GetProperty(\"x\").GetInt32(), driverDrs.GetProperty(\"y\").GetInt32()); var driverTyres = windows[0].GetProperty(\"Tyres\"); Size driverTyresArea = new Size(driverTyres.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverTyresPosition = new Point(driverTyres.GetProperty(\"x\").GetInt32(), driverTyres.GetProperty(\"y\").GetInt32()); var driverName = windows[0].GetProperty(\"Name\"); Size driverNameArea = new Size(driverName.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverNamePosition = new Point(driverName.GetProperty(\"x\").GetInt32(), driverName.GetProperty(\"y\").GetInt32()); var driverSector1 = windows[0].GetProperty(\"Sector1\"); Size driverSector1Area = new Size(driverSector1.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector1Position = new Point(driverSector1.GetProperty(\"x\").GetInt32(), driverSector1.GetProperty(\"y\").GetInt32()); var driverSector2 = windows[0].GetProperty(\"Sector2\"); Size driverSector2Area = new Size(driverSector2.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector2Position = new Point(driverSector2.GetProperty(\"x\").GetInt32(), driverSector2.GetProperty(\"y\").GetInt32()); var driverSector3 = windows[0].GetProperty(\"Sector3\"); Size driverSector3Area = new Size(driverSector3.GetProperty(\"width\").GetInt32(), FirstZoneSize.Height); Point driverSector3Position = new Point(driverSector3.GetProperty(\"x\").GetInt32(), driverSector3.GetProperty(\"y\").GetInt32()); float offset = (((float)mainZone.ZoneImage.Height - (float)(driverListToFill.Count * FirstZoneSize.Height)) / (float)driverListToFill.Count); Bitmap MainZoneImage = mainZone.ZoneImage; List<Zone> zonesToAdd = new List<Zone>(); List<Bitmap> zonesImages = new List<Bitmap>(); for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset)); Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize), \"DriverZone\"); zonesToAdd.Add(newDriverZone); zonesImages.Add(newDriverZone.ZoneImage); newDriverZone.ZoneImage.Save(\"Driver\"+i+\".png\"); } //Parallel.For(0, NUMBER_OF_DRIVERS, i => for (int i = 0; i < NUMBER_OF_DRIVERS; i++) { Zone newDriverZone = zonesToAdd[(int)i]; Bitmap zoneImg = zonesImages[(int)i]; newDriverZone.AddWindow(new DriverPositionWindow(zoneImg, new Rectangle(driverPositionPosition, driverPositionArea),LoadOCR)); newDriverZone.AddWindow(new DriverGapToLeaderWindow(zoneImg, new Rectangle(driverGapToLeaderPosition, driverGapToLeaderArea), LoadOCR)); newDriverZone.AddWindow(new DriverLapTimeWindow(zoneImg, new Rectangle(driverLapTimePosition, driverLapTimeArea), LoadOCR)); newDriverZone.AddWindow(new DriverDrsWindow(zoneImg, new Rectangle(driverDrsPosition, driverDrsArea), LoadOCR)); newDriverZone.AddWindow(new DriverTyresWindow(zoneImg, new Rectangle(driverTyresPosition, driverTyresArea), LoadOCR)); newDriverZone.AddWindow(new DriverNameWindow(zoneImg, new Rectangle(driverNamePosition, driverNameArea), LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector1Position, driverSector1Area),1, LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector2Position, driverSector2Area),2, LoadOCR)); newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector3Position, driverSector3Area),3, LoadOCR)); mainZone.AddZone(newDriverZone); }//); //MessageBox.Show(\"We have a main zone with \" + MainZone.Zones.Count() + \" Driver zones with \" + MainZone.Zones[4].Windows.Count() + \" windows each and we have \" + Drivers.Count + \" drivers\"); mainZones.Add(mainZone); } } catch (IOException ex) { MessageBox.Show(\"Error reading JSON file: \" + ex.Message); } catch (JsonException ex) { MessageBox.Show(\"Invalid JSON format: \" + ex.Message); } return mainZones; } /// <summary> /// Method that calls all the zones and windows to get the content they can find on the image to display them /// </summary> /// <param name=\"idImage\">The id of the image we are working with</param> /// <returns>a string representation of all the returns</returns> public async Task<string> Decode(List<Zone> mainZones,List<string> drivers) { string result = \"\"; List<DriverData> mainResults = new List<DriverData>(); //Decode for (int mainZoneId = 0; mainZoneId < mainZones.Count; mainZoneId++) { switch (mainZoneId) { case 0: //Main Zone foreach (Zone z in mainZones[mainZoneId].Zones) { mainResults.Add(await z.Decode(Drivers)); } break; //Next there could be a Title Zone and TrackInfoZone } } //Display foreach (DriverData driver in mainResults) { result += driver.ToString(); result += Environment.NewLine; } return result; } /// <summary> /// Method that can be used to convert an amount of miliseconds into a more readable human form /// </summary> /// <param name=\"amountOfMs\">The given amount of miliseconds ton convert</param> /// <returns>A human readable string that represents the ms</returns> public static string ConvertMsToTime(int amountOfMs) { //Convert.ToInt32 would round upand I dont want that int minuts = (int)((float)amountOfMs / (1000f * 60f)); int seconds = (int)((amountOfMs - (minuts * 60f * 1000f)) / 1000); int ms = amountOfMs - ((minuts * 60 * 1000) + (seconds * 1000)); return minuts + \":\" + seconds.ToString(\"00\") + \":\" + ms.ToString(\"000\"); } /// <summary> /// Old method that can draw on an image where the windows and zones are created. mostly used for debugging /// </summary> /// <param name=\"idImage\">the #id of the image we are working with</param> /// <returns>the drawed bitmap</returns> public Bitmap Draw(Bitmap image,List<Zone> mainZones) { Graphics g = Graphics.FromImage(image); foreach (Zone z in mainZones) { int count = 0; foreach (Zone zz in z.Zones) { g.DrawRectangle(Pens.Red, z.Bounds); foreach (Window w in zz.Windows) { g.DrawRectangle(Pens.Blue, new Rectangle(z.Bounds.X + zz.Bounds.X, z.Bounds.Y + zz.Bounds.Y, zz.Bounds.Width, zz.Bounds.Height)); } count++; } } return image; } } }","title":"Reader.cs"},{"location":"Code/Zone.html","text":"Zone.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : Zone.cs /// Brief : Class that contains all the methods and infos for a zone. This is designed to be potentially be inherited. /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { public class Zone { private Rectangle _bounds; private List<Zone> _zones; private List<Window> _windows; private Bitmap _image; private string _name; public Bitmap ZoneImage { get { //This little trickery lets you have the image that the zone sees Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Bitmap Image { get { return _image; } set { //It automatically sets the image for the contained windows and zones _image = Image; foreach (Window w in Windows) { w.Image = ZoneImage; } foreach (Zone z in Zones) { z.Image = Image; } } } public Rectangle Bounds { get => _bounds; protected set => _bounds = value; } public List<Zone> Zones { get => _zones; protected set => _zones = value; } public List<Window> Windows { get => _windows; protected set => _windows = value; } public string Name { get => _name; protected set => _name = value; } public Zone(Bitmap image, Rectangle bounds, string name) { Windows = new List<Window>(); Zones = new List<Zone>(); Name = name; //You cant set the image in the CTOR because the processing is impossible at first initiation _image = image; Bounds = bounds; } /// <summary> /// Adds a zone to the list of zones /// </summary> /// <param name=\"zone\">The zone you want to add</param> public virtual void AddZone(Zone zone) { Zones.Add(zone); } /// <summary> /// Add a window to the list of windows /// </summary> /// <param name=\"window\">the window you want to add</param> public virtual void AddWindow(Window window) { Windows.Add(window); } /// <summary> /// Calls all the windows to do OCR and to give back the results so we can send them to the model /// </summary> /// <param name=\"driverList\">A list of all the driver in the race to help with text recognition</param> /// <returns>A driver data object that contains all the infos about a driver</returns> 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; } public virtual Bitmap Draw() { Bitmap img; //If its the main zone we want to see everything if (Zones.Count > 0) { img = Image; } else { img = ZoneImage; } Graphics g = Graphics.FromImage(img); //If its the main zone we need to visualize the Zone bounds displayed if (Zones.Count > 0) g.DrawRectangle(new Pen(Brushes.Violet, 5), Bounds); foreach (Zone z in Zones) { Rectangle newBounds = new Rectangle(z.Bounds.X, z.Bounds.Y + Bounds.Y, z.Bounds.Width, z.Bounds.Height); g.DrawRectangle(Pens.Red, newBounds); } foreach (Window w in Windows) { g.DrawRectangle(Pens.Blue, w.Bounds); } return img; } public void ResetZones() { Zones.Clear(); } public void ResetWindows() { foreach (Zone z in Zones) { z.ResetWindows(); } Windows.Clear(); } public virtual string ToJSON() { string result = \"\"; result += \"\\\"\" + Name + \"\\\":{\" + Environment.NewLine; result += \"\\t\" + \"\\\"x\\\":\" + Bounds.X + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"y\\\":\" + Bounds.Y + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"width\\\":\" + Bounds.Width + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"height\\\":\" + Bounds.Height; if (Windows.Count != 0) { result += \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"Windows\\\":[\" + Environment.NewLine; result += \"\\t\\t{\" + Environment.NewLine; int Wcount = 0; foreach (Window w in Windows) { result += \"\\t\\t\" + w.ToJSON(); Wcount++; if (Wcount != Windows.Count) result += \",\"; } result += \"\\t\\t}\" + Environment.NewLine; result += \"\\t\" + \"]\" + Environment.NewLine; } else { result += Environment.NewLine; } if (Zones.Count != 0) { result += \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"Zones\\\":[\" + Environment.NewLine; result += \"\\t\\t{\" + Environment.NewLine; int Zcount = 0; //foreach (Zone z in Zones) //{ result += \"\\t\\t\" + Zones[0].ToJSON(); Zcount++; if (Zcount != Zones.Count) //result += \",\"; //} result += \"\\t\\t}\" + Environment.NewLine; result += \"\\t\" + \"]\" + Environment.NewLine; } else { result += Environment.NewLine; } result += \"}\"; return result; } /// <summary> /// Checks if the given Rectangle fits in the current zone /// </summary> /// <param name=\"InputRectangle\">The Rectangle you want to check the fittment</param> /// <returns></returns> protected 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; } } } }","title":"Zone.cs"},{"location":"Code/Zone.html#zonecs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : Zone.cs /// Brief : Class that contains all the methods and infos for a zone. This is designed to be potentially be inherited. /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test_Merge { public class Zone { private Rectangle _bounds; private List<Zone> _zones; private List<Window> _windows; private Bitmap _image; private string _name; public Bitmap ZoneImage { get { //This little trickery lets you have the image that the zone sees Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } public Bitmap Image { get { return _image; } set { //It automatically sets the image for the contained windows and zones _image = Image; foreach (Window w in Windows) { w.Image = ZoneImage; } foreach (Zone z in Zones) { z.Image = Image; } } } public Rectangle Bounds { get => _bounds; protected set => _bounds = value; } public List<Zone> Zones { get => _zones; protected set => _zones = value; } public List<Window> Windows { get => _windows; protected set => _windows = value; } public string Name { get => _name; protected set => _name = value; } public Zone(Bitmap image, Rectangle bounds, string name) { Windows = new List<Window>(); Zones = new List<Zone>(); Name = name; //You cant set the image in the CTOR because the processing is impossible at first initiation _image = image; Bounds = bounds; } /// <summary> /// Adds a zone to the list of zones /// </summary> /// <param name=\"zone\">The zone you want to add</param> public virtual void AddZone(Zone zone) { Zones.Add(zone); } /// <summary> /// Add a window to the list of windows /// </summary> /// <param name=\"window\">the window you want to add</param> public virtual void AddWindow(Window window) { Windows.Add(window); } /// <summary> /// Calls all the windows to do OCR and to give back the results so we can send them to the model /// </summary> /// <param name=\"driverList\">A list of all the driver in the race to help with text recognition</param> /// <returns>A driver data object that contains all the infos about a driver</returns> 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; } public virtual Bitmap Draw() { Bitmap img; //If its the main zone we want to see everything if (Zones.Count > 0) { img = Image; } else { img = ZoneImage; } Graphics g = Graphics.FromImage(img); //If its the main zone we need to visualize the Zone bounds displayed if (Zones.Count > 0) g.DrawRectangle(new Pen(Brushes.Violet, 5), Bounds); foreach (Zone z in Zones) { Rectangle newBounds = new Rectangle(z.Bounds.X, z.Bounds.Y + Bounds.Y, z.Bounds.Width, z.Bounds.Height); g.DrawRectangle(Pens.Red, newBounds); } foreach (Window w in Windows) { g.DrawRectangle(Pens.Blue, w.Bounds); } return img; } public void ResetZones() { Zones.Clear(); } public void ResetWindows() { foreach (Zone z in Zones) { z.ResetWindows(); } Windows.Clear(); } public virtual string ToJSON() { string result = \"\"; result += \"\\\"\" + Name + \"\\\":{\" + Environment.NewLine; result += \"\\t\" + \"\\\"x\\\":\" + Bounds.X + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"y\\\":\" + Bounds.Y + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"width\\\":\" + Bounds.Width + \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"height\\\":\" + Bounds.Height; if (Windows.Count != 0) { result += \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"Windows\\\":[\" + Environment.NewLine; result += \"\\t\\t{\" + Environment.NewLine; int Wcount = 0; foreach (Window w in Windows) { result += \"\\t\\t\" + w.ToJSON(); Wcount++; if (Wcount != Windows.Count) result += \",\"; } result += \"\\t\\t}\" + Environment.NewLine; result += \"\\t\" + \"]\" + Environment.NewLine; } else { result += Environment.NewLine; } if (Zones.Count != 0) { result += \",\" + Environment.NewLine; result += \"\\t\" + \"\\\"Zones\\\":[\" + Environment.NewLine; result += \"\\t\\t{\" + Environment.NewLine; int Zcount = 0; //foreach (Zone z in Zones) //{ result += \"\\t\\t\" + Zones[0].ToJSON(); Zcount++; if (Zcount != Zones.Count) //result += \",\"; //} result += \"\\t\\t}\" + Environment.NewLine; result += \"\\t\" + \"]\" + Environment.NewLine; } else { result += Environment.NewLine; } result += \"}\"; return result; } /// <summary> /// Checks if the given Rectangle fits in the current zone /// </summary> /// <param name=\"InputRectangle\">The Rectangle you want to check the fittment</param> /// <returns></returns> protected 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; } } } }","title":"Zone.cs"},{"location":"Code/DriverDrsWindow.html","text":"DriverDrsWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverDrsWindow.cs /// Brief : Window containing DRS related method and infos /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.Linq; using System.Text; using System.Threading.Tasks; using Tesseract; namespace Test_Merge { internal class DriverDrsWindow:Window { private static int EmptyDrsGreenValue = -1; private static Random rnd = new Random(); public DriverDrsWindow(Bitmap image, Rectangle bounds,bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"DRS\"; } public override async Task<object> DecodePng() { bool result = false; int greenValue = GetGreenPixels(); if (EmptyDrsGreenValue == -1) EmptyDrsGreenValue = greenValue; if (greenValue > EmptyDrsGreenValue + EmptyDrsGreenValue / 100 * 30) result = true; return result; } private unsafe int GetGreenPixels() { int tot = 0; Bitmap bmp = WindowImage; Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < bmp.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmp.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[2]; if (green > blue * 1.5 && green > red * 1.5) { tot++; } } } } bmp.UnlockBits(bmpData); return tot; } public Rectangle GetBox() { var tessImage = Pix.LoadFromMemory(ImageToByte(WindowImage)); Engine.SetVariable(\"tessedit_char_whitelist\", \"\"); Page page = Engine.Process(tessImage); using (var iter = page.GetIterator()) { iter.Begin(); do { Rect boundingBox; // Get the bounding box for the current element if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { page.Dispose(); return new Rectangle(boundingBox.X1, boundingBox.X2, boundingBox.Width, boundingBox.Height); } } while (iter.Next(PageIteratorLevel.Word)); page.Dispose(); return new Rectangle(0, 0, 0, 0); } } } }","title":"DriverDrsWindow.cs"},{"location":"Code/DriverDrsWindow.html#driverdrswindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverDrsWindow.cs /// Brief : Window containing DRS related method and infos /// Version : 0.1 using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.Linq; using System.Text; using System.Threading.Tasks; using Tesseract; namespace Test_Merge { internal class DriverDrsWindow:Window { private static int EmptyDrsGreenValue = -1; private static Random rnd = new Random(); public DriverDrsWindow(Bitmap image, Rectangle bounds,bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"DRS\"; } public override async Task<object> DecodePng() { bool result = false; int greenValue = GetGreenPixels(); if (EmptyDrsGreenValue == -1) EmptyDrsGreenValue = greenValue; if (greenValue > EmptyDrsGreenValue + EmptyDrsGreenValue / 100 * 30) result = true; return result; } private unsafe int GetGreenPixels() { int tot = 0; Bitmap bmp = WindowImage; Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < bmp.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmp.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[2]; if (green > blue * 1.5 && green > red * 1.5) { tot++; } } } } bmp.UnlockBits(bmpData); return tot; } public Rectangle GetBox() { var tessImage = Pix.LoadFromMemory(ImageToByte(WindowImage)); Engine.SetVariable(\"tessedit_char_whitelist\", \"\"); Page page = Engine.Process(tessImage); using (var iter = page.GetIterator()) { iter.Begin(); do { Rect boundingBox; // Get the bounding box for the current element if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox)) { page.Dispose(); return new Rectangle(boundingBox.X1, boundingBox.X2, boundingBox.Width, boundingBox.Height); } } while (iter.Next(PageIteratorLevel.Word)); page.Dispose(); return new Rectangle(0, 0, 0, 0); } } } }","title":"DriverDrsWindow.cs"},{"location":"Code/DriverNameWindow.html","text":"DriverNameWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverNameWindow /// Brief : Window containing infos about the name of the driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverNameWindow : Window { public static Random rnd = new Random(); public DriverNameWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Name\"; } /// <summary> /// Decodes using OCR wich driver name is in the image /// </summary> /// <param name=\"DriverList\"></param> /// <returns>The driver name in string</returns> public override async Task<object> DecodePng(List<string> DriverList) { string result = \"\"; result = await GetStringFromPng(WindowImage, Engine); if (!IsADriver(DriverList, result)) { //I put everything in uppercase to try to lower the chances of bad answers result = FindClosestMatch(DriverList.ConvertAll(d => d.ToUpper()), result.ToUpper()); } return result; } /// <summary> /// Verifies that the name found in the OCR is a valid name /// </summary> /// <param name=\"driverList\"></param> /// <param name=\"potentialDriver\"></param> /// <returns>If ye or no the driver exists</returns> private static bool IsADriver(List<string> driverList, string potentialDriver) { bool result = false; //I cant use drivers.Contains because it has missmatched cases and all foreach (string name in driverList) { if (name.ToUpper() == potentialDriver.ToUpper()) result = true; } return result; } } }","title":"DriverNameWindow.cs"},{"location":"Code/DriverNameWindow.html#drivernamewindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverNameWindow /// Brief : Window containing infos about the name of the driver /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverNameWindow : Window { public static Random rnd = new Random(); public DriverNameWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Name\"; } /// <summary> /// Decodes using OCR wich driver name is in the image /// </summary> /// <param name=\"DriverList\"></param> /// <returns>The driver name in string</returns> public override async Task<object> DecodePng(List<string> DriverList) { string result = \"\"; result = await GetStringFromPng(WindowImage, Engine); if (!IsADriver(DriverList, result)) { //I put everything in uppercase to try to lower the chances of bad answers result = FindClosestMatch(DriverList.ConvertAll(d => d.ToUpper()), result.ToUpper()); } return result; } /// <summary> /// Verifies that the name found in the OCR is a valid name /// </summary> /// <param name=\"driverList\"></param> /// <param name=\"potentialDriver\"></param> /// <returns>If ye or no the driver exists</returns> private static bool IsADriver(List<string> driverList, string potentialDriver) { bool result = false; //I cant use drivers.Contains because it has missmatched cases and all foreach (string name in driverList) { if (name.ToUpper() == potentialDriver.ToUpper()) result = true; } return result; } } }","title":"DriverNameWindow.cs"},{"location":"Code/DriverTyresWindow.html","text":"DriverTyresWindow.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverTyresWindow.cs /// Brief : Window containing infos about a driver's tyre /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverTyresWindow:Window { private static Random rnd = new Random(); int seed = rnd.Next(0, 10000); //Those are the colors I found but you can change them if they change in the future like in 2019 public static Color SOFT_TYRE_COLOR = Color.FromArgb(0xff, 0x00, 0x00); public static Color MEDIUM_TYRE_COLOR = Color.FromArgb(0xf5, 0xbf, 0x00); public static Color HARD_TYRE_COLOR = Color.FromArgb(0xa4, 0xa5, 0xa8); public static Color INTER_TYRE_COLOR = Color.FromArgb(0x00, 0xa4, 0x2e); public static Color WET_TYRE_COLOR = Color.FromArgb(0x27, 0x60, 0xa6); public static Color EMPTY_COLOR = Color.FromArgb(0x20, 0x20, 0x20); public DriverTyresWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Tyres\"; } /// <summary> /// This will decode the content of the image /// </summary> /// <returns>And object containing what was on the image</returns> public override async Task<object> DecodePng() { return await GetTyreInfos(); } /// <summary> /// Method that will decode whats on the image and return the tyre infos it could manage to recover /// </summary> /// <returns>A tyre object containing tyre infos</returns> private async Task<Tyre> GetTyreInfos() { Bitmap tyreZone = GetSmallBitmapFromBigOne(WindowImage, FindTyreZone()); Tyre.Type type = Tyre.Type.Undefined; type = GetTyreTypeFromColor(OcrImage.GetAvgColorFromBitmap(tyreZone)); int laps = -1; string number = await GetStringFromPng(tyreZone, Engine, \"0123456789\", OcrImage.WindowType.Tyre); try { laps = Convert.ToInt32(number); } catch { //We could not convert the number so its a letter so its 0 laps old laps = 0; } //tyreZone.Save(Reader.DEBUG_DUMP_FOLDER + \"Tyre\" + type + \"Laps\" + laps + '#' + rnd.Next(0, 1000) + \".png\"); return new Tyre(type, laps); } /// <summary> /// Finds where the important part of the image is /// </summary> /// <returns>A rectangle containing position and dimensions of the important part of the image</returns> 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 - Convert.ToInt32((float)bmp.Height / 100f * 25f), bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 35f)); 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 int CorrectedX = currentPosition - (newWindowSize.Width) + Convert.ToInt32((float)newWindowSize.Width / 100f * 10f); int CorrectedY = Convert.ToInt32((float)newWindowSize.Height / 100f * 35f); if (CorrectedX <= 0) return new Rectangle(0, 0, newWindowSize.Width, newWindowSize.Height); return new Rectangle(CorrectedX, CorrectedY, newWindowSize.Width, newWindowSize.Height); } //This method has been created with the help of chatGPT /// <summary> /// Methods that compares a list of colors to see wich is the closest from the input color and decide wich tyre type it is /// </summary> /// <param name=\"inputColor\">The color that you found</param> /// <returns>The tyre type</returns> public Tyre.Type GetTyreTypeFromColor(Color inputColor) { Tyre.Type type = Tyre.Type.Undefined; List<Color> colors = new List<Color>(); //dont forget that if for some reason someday F1 adds a new Tyre type you will need to add it in the constants but also here in the list //You will also need to add it below in the Tyre object's enum and add an if in the end of this method colors.Add(SOFT_TYRE_COLOR); colors.Add(MEDIUM_TYRE_COLOR); colors.Add(HARD_TYRE_COLOR); colors.Add(INTER_TYRE_COLOR); colors.Add(WET_TYRE_COLOR); colors.Add(EMPTY_COLOR); Color closestColor = colors[0]; int closestDistance = int.MaxValue; foreach (Color color in colors) { int distance = Math.Abs(color.R - inputColor.R) + Math.Abs(color.G - inputColor.G) + Math.Abs(color.B - inputColor.B); if (distance < closestDistance) { closestColor = color; closestDistance = distance; } } //We cant use a switch as the colors cant be constants ... if (closestColor == SOFT_TYRE_COLOR) type = Tyre.Type.Soft; if (closestColor == MEDIUM_TYRE_COLOR) type = Tyre.Type.Medium; if (closestColor == HARD_TYRE_COLOR) type = Tyre.Type.Hard; if (closestColor == INTER_TYRE_COLOR) type = Tyre.Type.Inter; if (closestColor == WET_TYRE_COLOR) type = Tyre.Type.Wet; if (closestColor == EMPTY_COLOR) return Tyre.Type.Undefined; return type; } } }","title":"DriverTyresWindow.cs"},{"location":"Code/DriverTyresWindow.html#drivertyreswindowcs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : DriverTyresWindow.cs /// Brief : Window containing infos about a driver's tyre /// Version : 0.1 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; namespace Test_Merge { public class DriverTyresWindow:Window { private static Random rnd = new Random(); int seed = rnd.Next(0, 10000); //Those are the colors I found but you can change them if they change in the future like in 2019 public static Color SOFT_TYRE_COLOR = Color.FromArgb(0xff, 0x00, 0x00); public static Color MEDIUM_TYRE_COLOR = Color.FromArgb(0xf5, 0xbf, 0x00); public static Color HARD_TYRE_COLOR = Color.FromArgb(0xa4, 0xa5, 0xa8); public static Color INTER_TYRE_COLOR = Color.FromArgb(0x00, 0xa4, 0x2e); public static Color WET_TYRE_COLOR = Color.FromArgb(0x27, 0x60, 0xa6); public static Color EMPTY_COLOR = Color.FromArgb(0x20, 0x20, 0x20); public DriverTyresWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine) { Name = \"Tyres\"; } /// <summary> /// This will decode the content of the image /// </summary> /// <returns>And object containing what was on the image</returns> public override async Task<object> DecodePng() { return await GetTyreInfos(); } /// <summary> /// Method that will decode whats on the image and return the tyre infos it could manage to recover /// </summary> /// <returns>A tyre object containing tyre infos</returns> private async Task<Tyre> GetTyreInfos() { Bitmap tyreZone = GetSmallBitmapFromBigOne(WindowImage, FindTyreZone()); Tyre.Type type = Tyre.Type.Undefined; type = GetTyreTypeFromColor(OcrImage.GetAvgColorFromBitmap(tyreZone)); int laps = -1; string number = await GetStringFromPng(tyreZone, Engine, \"0123456789\", OcrImage.WindowType.Tyre); try { laps = Convert.ToInt32(number); } catch { //We could not convert the number so its a letter so its 0 laps old laps = 0; } //tyreZone.Save(Reader.DEBUG_DUMP_FOLDER + \"Tyre\" + type + \"Laps\" + laps + '#' + rnd.Next(0, 1000) + \".png\"); return new Tyre(type, laps); } /// <summary> /// Finds where the important part of the image is /// </summary> /// <returns>A rectangle containing position and dimensions of the important part of the image</returns> 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 - Convert.ToInt32((float)bmp.Height / 100f * 25f), bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 35f)); 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 int CorrectedX = currentPosition - (newWindowSize.Width) + Convert.ToInt32((float)newWindowSize.Width / 100f * 10f); int CorrectedY = Convert.ToInt32((float)newWindowSize.Height / 100f * 35f); if (CorrectedX <= 0) return new Rectangle(0, 0, newWindowSize.Width, newWindowSize.Height); return new Rectangle(CorrectedX, CorrectedY, newWindowSize.Width, newWindowSize.Height); } //This method has been created with the help of chatGPT /// <summary> /// Methods that compares a list of colors to see wich is the closest from the input color and decide wich tyre type it is /// </summary> /// <param name=\"inputColor\">The color that you found</param> /// <returns>The tyre type</returns> public Tyre.Type GetTyreTypeFromColor(Color inputColor) { Tyre.Type type = Tyre.Type.Undefined; List<Color> colors = new List<Color>(); //dont forget that if for some reason someday F1 adds a new Tyre type you will need to add it in the constants but also here in the list //You will also need to add it below in the Tyre object's enum and add an if in the end of this method colors.Add(SOFT_TYRE_COLOR); colors.Add(MEDIUM_TYRE_COLOR); colors.Add(HARD_TYRE_COLOR); colors.Add(INTER_TYRE_COLOR); colors.Add(WET_TYRE_COLOR); colors.Add(EMPTY_COLOR); Color closestColor = colors[0]; int closestDistance = int.MaxValue; foreach (Color color in colors) { int distance = Math.Abs(color.R - inputColor.R) + Math.Abs(color.G - inputColor.G) + Math.Abs(color.B - inputColor.B); if (distance < closestDistance) { closestColor = color; closestDistance = distance; } } //We cant use a switch as the colors cant be constants ... if (closestColor == SOFT_TYRE_COLOR) type = Tyre.Type.Soft; if (closestColor == MEDIUM_TYRE_COLOR) type = Tyre.Type.Medium; if (closestColor == HARD_TYRE_COLOR) type = Tyre.Type.Hard; if (closestColor == INTER_TYRE_COLOR) type = Tyre.Type.Inter; if (closestColor == WET_TYRE_COLOR) type = Tyre.Type.Wet; if (closestColor == EMPTY_COLOR) return Tyre.Type.Undefined; return type; } } }","title":"DriverTyresWindow.cs"},{"location":"Code/OcrImage.html","text":"OcrImage.cs \ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : OcrImage.cs /// Brief : Class containing all the methods used to enhance images for OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; namespace Test_Merge { public class OcrImage { //this is a hardcoded value based on the colors of the F1TV data channel background you can change it if sometime in the future the color changes //Any color that has any of its R,G or B channel higher than the treshold will be considered as being usefull information public static Color F1TV_BACKGROUND_TRESHOLD = Color.FromArgb(0x50, 0x50, 0x50); Bitmap InputBitmap; public enum WindowType { LapTime, Text, Sector, Gap, Tyre, } /// <summary> /// Create a new Ocr image to help enhance the given bitmap for OCR /// </summary> /// <param name=\"inputBitmap\">The image you want to enhance</param> public OcrImage(Bitmap inputBitmap) { InputBitmap = inputBitmap; } /// <summary> /// Enhances the image depending on wich type of window the image comes from /// </summary> /// <param name=\"type\">The type of the window. Depending on it different enhancing features will be applied</param> /// <returns>The enhanced Bitmap</returns> public Bitmap Enhance(WindowType type = WindowType.Text) { Bitmap outputBitmap = (Bitmap)InputBitmap.Clone(); switch (type) { case WindowType.LapTime: outputBitmap = Tresholding(outputBitmap, 185); outputBitmap = Resize(outputBitmap, 2); outputBitmap = Dilatation(outputBitmap, 1); outputBitmap = Erode(outputBitmap, 1); break; case WindowType.Text: outputBitmap = InvertColors(outputBitmap); outputBitmap = Tresholding(outputBitmap, 165); outputBitmap = Resize(outputBitmap, 2); outputBitmap = Dilatation(outputBitmap, 1); break; case WindowType.Tyre: outputBitmap = RemoveUseless(outputBitmap); outputBitmap = Resize(outputBitmap, 4); outputBitmap = Dilatation(outputBitmap, 1); break; default: outputBitmap = Tresholding(outputBitmap, 165); outputBitmap = Resize(outputBitmap, 4); outputBitmap = Erode(outputBitmap, 1); break; } return outputBitmap; } /// <summary> /// Method that convert a colored RGB bitmap into a GrayScale image /// </summary> /// <param name=\"inputBitmap\">The Bitmap you want to convert</param> /// <returns>The bitmap in grayscale</returns> public static Bitmap Grayscale(Bitmap inputBitmap) { Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[2]; //Those a specific values to correct the weights so its more pleasing to the human eye int gray = (int)(red * 0.3 + green * 0.59 + blue * 0.11); pixel[0] = pixel[1] = pixel[2] = (byte)gray; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// <summary> /// Method that binaries the input image up to a certain treshold given /// </summary> /// <param name=\"inputBitmap\">the bitmap you want to convert to binary colors</param> /// <param name=\"threshold\">The floor at wich the color is considered as white or black</param> /// <returns>The binarised bitmap</returns> public static Bitmap Tresholding(Bitmap inputBitmap, int threshold) { Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); int bmpHeight = inputBitmap.Height; int bmpWidth = inputBitmap.Width; Parallel.For(0, bmpHeight, y => { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmpWidth; x++) { byte* pixel = currentLine + (x * bytesPerPixel); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[2]; //Those a specific values to correct the weights so its more pleasing to the human eye int gray = (int)(red * 0.3 + green * 0.59 + blue * 0.11); int value = gray < threshold ? 0 : 255; pixel[0] = pixel[1] = pixel[2] = (byte)value; } }); } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// <summary> /// Method that removes the pixels that are flagged as background /// </summary> /// <param name=\"inputBitmap\">The bitmap you want to remove the background from</param> /// <returns>The Bitmap without the background</returns> public static Bitmap RemoveBG(Bitmap inputBitmap) { Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R <= F1TV_BACKGROUND_TRESHOLD.R && G <= F1TV_BACKGROUND_TRESHOLD.G && B <= F1TV_BACKGROUND_TRESHOLD.B) pixel[0] = pixel[1] = pixel[2] = 0; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// <summary> /// Method that removes all the useless things from the image and returns hopefully only the numbers /// </summary> /// <param name=\"inputBitmap\">The bitmap you want to remove useless things from (Expects a cropped part of the TyreWindow)</param> /// <returns>The bitmap with (hopefully) only the digits</returns> public unsafe static Bitmap RemoveUseless(Bitmap inputBitmap) { //Note you can use something else than a cropped tyre window but I would recommend checking the code first to see if it fits your intended use Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); List<int> pixelsToRemove = new List<int>(); bool fromBorder = true; for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) { pixelsToRemove.Add(x); } else { if (fromBorder) { fromBorder = false; pixelsToRemove.Add(x); } } } fromBorder = true; for (int x = inputBitmap.Width - 1; x > 0; x--) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) { pixelsToRemove.Add(x); } else { if (fromBorder) { fromBorder = false; pixelsToRemove.Add(x); } } } foreach (int pxPos in pixelsToRemove) { byte* pixel = currentLine + (pxPos * bytesPerPixel); pixel[0] = 0xFF; pixel[1] = 0xFF; pixel[2] = 0xFF; } } //Removing the color parts for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R >= F1TV_BACKGROUND_TRESHOLD.R + 15 || G >= F1TV_BACKGROUND_TRESHOLD.G + 15 || B >= F1TV_BACKGROUND_TRESHOLD.B + 15) { pixel[0] = 0xFF; pixel[1] = 0xFF; pixel[2] = 0xFF; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// <summary> /// Recovers the average colors from the Image. NOTE : It wont take in account colors that are lower than the background /// </summary> /// <param name=\"inputBitmap\">The bitmap you want to get the average color from</param> /// <returns>The average color of the bitmap</returns> public static Color GetAvgColorFromBitmap(Bitmap inputBitmap) { Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; int totR = 0; int totG = 0; int totB = 0; int totPixels = 1; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); int bmpHeight = inputBitmap.Height; int bmpWidth = inputBitmap.Width; Parallel.For(0, bmpHeight, y => { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmpWidth; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R >= F1TV_BACKGROUND_TRESHOLD.R || G >= F1TV_BACKGROUND_TRESHOLD.G || B >= F1TV_BACKGROUND_TRESHOLD.B) { totPixels++; totB += pixel[0]; totG += pixel[1]; totR += pixel[2]; } } }); } inputBitmap.UnlockBits(bmpData); return Color.FromArgb(255, Convert.ToInt32((float)totR / (float)totPixels), Convert.ToInt32((float)totG / (float)totPixels), Convert.ToInt32((float)totB / (float)totPixels)); } /// <summary> /// This method simply inverts all the colors in a Bitmap /// </summary> /// <param name=\"inputBitmap\">the bitmap you want to invert the colors from</param> /// <returns>The bitmap with inverted colors</returns> public static Bitmap InvertColors(Bitmap inputBitmap) { Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); pixel[0] = (byte)(255 - pixel[0]); pixel[1] = (byte)(255 - pixel[1]); pixel[2] = (byte)(255 - pixel[2]); } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// <summary> /// Methods that applies Bicubic interpolation to increase the size and resolution of an image /// </summary> /// <param name=\"inputBitmap\">The bitmap you want to resize</param> /// <param name=\"resizeFactor\">The factor of resizing you want to use. I recommend using even numbers</param> /// <returns>The bitmap witht the new size</returns> public static Bitmap Resize(Bitmap inputBitmap, int resizeFactor) { var resultBitmap = new Bitmap(inputBitmap.Width * resizeFactor, inputBitmap.Height * resizeFactor); using (var graphics = Graphics.FromImage(resultBitmap)) { graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.DrawImage(inputBitmap, new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height)); } return resultBitmap; } /// <summary> /// method that Highlights the countours of a Bitmap /// </summary> /// <param name=\"inputBitmap\">The bitmap you want to highlight the countours of</param> /// <returns>The bitmap with countours highlighted</returns> public static Bitmap HighlightContours(Bitmap inputBitmap) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); Bitmap grayscale = Grayscale(inputBitmap); Bitmap thresholded = Tresholding(grayscale, 128); Bitmap dilated = Dilatation(thresholded, 3); Bitmap eroded = Erode(dilated, 3); for (int y = 0; y < inputBitmap.Height; y++) { for (int x = 0; x < inputBitmap.Width; x++) { Color pixel = inputBitmap.GetPixel(x, y); Color dilatedPixel = dilated.GetPixel(x, y); Color erodedPixel = eroded.GetPixel(x, y); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); int threshold = dilatedPixel.R; if (gray > threshold) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } else if (gray <= threshold && erodedPixel.R == 0) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 0, 0)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } } } return outputBitmap; } /// <summary> /// Method that that erodes the morphology of a bitmap /// </summary> /// <param name=\"inputBitmap\">The bitmap you want to erode</param> /// <param name=\"kernelSize\">The amount of Erosion you want (be carefull its expensive on ressources)</param> /// <returns>The Bitmap with the eroded contents</returns> public static Bitmap Erode(Bitmap inputBitmap, int kernelSize) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); int[,] kernel = new int[kernelSize, kernelSize]; for (int i = 0; i < kernelSize; i++) { for (int j = 0; j < kernelSize; j++) { kernel[i, j] = 1; } } for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) { for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) { bool flag = true; for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) { for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) { Color pixel = inputBitmap.GetPixel(x + i, y + j); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); if (gray >= 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1) { flag = false; break; } } if (!flag) { break; } } if (flag) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } } } return outputBitmap; } /// <summary> /// Method that that use dilatation of the morphology of a bitmap /// </summary> /// <param name=\"inputBitmap\">The bitmap you want to use dilatation on</param> /// <param name=\"kernelSize\">The amount of dilatation you want (be carefull its expensive on ressources)</param> /// <returns>The Bitmap after Dilatation</returns> public static Bitmap Dilatation(Bitmap inputBitmap, int kernelSize) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); int[,] kernel = new int[kernelSize, kernelSize]; for (int i = 0; i < kernelSize; i++) { for (int j = 0; j < kernelSize; j++) { kernel[i, j] = 1; } } for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) { for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) { bool flag = false; for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) { for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) { Color pixel = inputBitmap.GetPixel(x + i, y + j); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); if (gray < 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1) { flag = true; break; } } if (flag) { break; } } if (flag) { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } } } return outputBitmap; } } }","title":"OcrImage.cs"},{"location":"Code/OcrImage.html#ocrimagecs","text":"\ufeff/// Author : Maxime Rohmer /// Date : 08/05/2023 /// File : OcrImage.cs /// Brief : Class containing all the methods used to enhance images for OCR /// Version : 0.1 using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; namespace Test_Merge { public class OcrImage { //this is a hardcoded value based on the colors of the F1TV data channel background you can change it if sometime in the future the color changes //Any color that has any of its R,G or B channel higher than the treshold will be considered as being usefull information public static Color F1TV_BACKGROUND_TRESHOLD = Color.FromArgb(0x50, 0x50, 0x50); Bitmap InputBitmap; public enum WindowType { LapTime, Text, Sector, Gap, Tyre, } /// <summary> /// Create a new Ocr image to help enhance the given bitmap for OCR /// </summary> /// <param name=\"inputBitmap\">The image you want to enhance</param> public OcrImage(Bitmap inputBitmap) { InputBitmap = inputBitmap; } /// <summary> /// Enhances the image depending on wich type of window the image comes from /// </summary> /// <param name=\"type\">The type of the window. Depending on it different enhancing features will be applied</param> /// <returns>The enhanced Bitmap</returns> public Bitmap Enhance(WindowType type = WindowType.Text) { Bitmap outputBitmap = (Bitmap)InputBitmap.Clone(); switch (type) { case WindowType.LapTime: outputBitmap = Tresholding(outputBitmap, 185); outputBitmap = Resize(outputBitmap, 2); outputBitmap = Dilatation(outputBitmap, 1); outputBitmap = Erode(outputBitmap, 1); break; case WindowType.Text: outputBitmap = InvertColors(outputBitmap); outputBitmap = Tresholding(outputBitmap, 165); outputBitmap = Resize(outputBitmap, 2); outputBitmap = Dilatation(outputBitmap, 1); break; case WindowType.Tyre: outputBitmap = RemoveUseless(outputBitmap); outputBitmap = Resize(outputBitmap, 4); outputBitmap = Dilatation(outputBitmap, 1); break; default: outputBitmap = Tresholding(outputBitmap, 165); outputBitmap = Resize(outputBitmap, 4); outputBitmap = Erode(outputBitmap, 1); break; } return outputBitmap; } /// <summary> /// Method that convert a colored RGB bitmap into a GrayScale image /// </summary> /// <param name=\"inputBitmap\">The Bitmap you want to convert</param> /// <returns>The bitmap in grayscale</returns> public static Bitmap Grayscale(Bitmap inputBitmap) { Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[2]; //Those a specific values to correct the weights so its more pleasing to the human eye int gray = (int)(red * 0.3 + green * 0.59 + blue * 0.11); pixel[0] = pixel[1] = pixel[2] = (byte)gray; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// <summary> /// Method that binaries the input image up to a certain treshold given /// </summary> /// <param name=\"inputBitmap\">the bitmap you want to convert to binary colors</param> /// <param name=\"threshold\">The floor at wich the color is considered as white or black</param> /// <returns>The binarised bitmap</returns> public static Bitmap Tresholding(Bitmap inputBitmap, int threshold) { Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); int bmpHeight = inputBitmap.Height; int bmpWidth = inputBitmap.Width; Parallel.For(0, bmpHeight, y => { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmpWidth; x++) { byte* pixel = currentLine + (x * bytesPerPixel); byte blue = pixel[0]; byte green = pixel[1]; byte red = pixel[2]; //Those a specific values to correct the weights so its more pleasing to the human eye int gray = (int)(red * 0.3 + green * 0.59 + blue * 0.11); int value = gray < threshold ? 0 : 255; pixel[0] = pixel[1] = pixel[2] = (byte)value; } }); } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// <summary> /// Method that removes the pixels that are flagged as background /// </summary> /// <param name=\"inputBitmap\">The bitmap you want to remove the background from</param> /// <returns>The Bitmap without the background</returns> public static Bitmap RemoveBG(Bitmap inputBitmap) { Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R <= F1TV_BACKGROUND_TRESHOLD.R && G <= F1TV_BACKGROUND_TRESHOLD.G && B <= F1TV_BACKGROUND_TRESHOLD.B) pixel[0] = pixel[1] = pixel[2] = 0; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// <summary> /// Method that removes all the useless things from the image and returns hopefully only the numbers /// </summary> /// <param name=\"inputBitmap\">The bitmap you want to remove useless things from (Expects a cropped part of the TyreWindow)</param> /// <returns>The bitmap with (hopefully) only the digits</returns> public unsafe static Bitmap RemoveUseless(Bitmap inputBitmap) { //Note you can use something else than a cropped tyre window but I would recommend checking the code first to see if it fits your intended use Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); List<int> pixelsToRemove = new List<int>(); bool fromBorder = true; for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) { pixelsToRemove.Add(x); } else { if (fromBorder) { fromBorder = false; pixelsToRemove.Add(x); } } } fromBorder = true; for (int x = inputBitmap.Width - 1; x > 0; x--) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) { pixelsToRemove.Add(x); } else { if (fromBorder) { fromBorder = false; pixelsToRemove.Add(x); } } } foreach (int pxPos in pixelsToRemove) { byte* pixel = currentLine + (pxPos * bytesPerPixel); pixel[0] = 0xFF; pixel[1] = 0xFF; pixel[2] = 0xFF; } } //Removing the color parts for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R >= F1TV_BACKGROUND_TRESHOLD.R + 15 || G >= F1TV_BACKGROUND_TRESHOLD.G + 15 || B >= F1TV_BACKGROUND_TRESHOLD.B + 15) { pixel[0] = 0xFF; pixel[1] = 0xFF; pixel[2] = 0xFF; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// <summary> /// Recovers the average colors from the Image. NOTE : It wont take in account colors that are lower than the background /// </summary> /// <param name=\"inputBitmap\">The bitmap you want to get the average color from</param> /// <returns>The average color of the bitmap</returns> public static Color GetAvgColorFromBitmap(Bitmap inputBitmap) { Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; int totR = 0; int totG = 0; int totB = 0; int totPixels = 1; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); int bmpHeight = inputBitmap.Height; int bmpWidth = inputBitmap.Width; Parallel.For(0, bmpHeight, y => { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmpWidth; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R >= F1TV_BACKGROUND_TRESHOLD.R || G >= F1TV_BACKGROUND_TRESHOLD.G || B >= F1TV_BACKGROUND_TRESHOLD.B) { totPixels++; totB += pixel[0]; totG += pixel[1]; totR += pixel[2]; } } }); } inputBitmap.UnlockBits(bmpData); return Color.FromArgb(255, Convert.ToInt32((float)totR / (float)totPixels), Convert.ToInt32((float)totG / (float)totPixels), Convert.ToInt32((float)totB / (float)totPixels)); } /// <summary> /// This method simply inverts all the colors in a Bitmap /// </summary> /// <param name=\"inputBitmap\">the bitmap you want to invert the colors from</param> /// <returns>The bitmap with inverted colors</returns> public static Bitmap InvertColors(Bitmap inputBitmap) { Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); pixel[0] = (byte)(255 - pixel[0]); pixel[1] = (byte)(255 - pixel[1]); pixel[2] = (byte)(255 - pixel[2]); } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// <summary> /// Methods that applies Bicubic interpolation to increase the size and resolution of an image /// </summary> /// <param name=\"inputBitmap\">The bitmap you want to resize</param> /// <param name=\"resizeFactor\">The factor of resizing you want to use. I recommend using even numbers</param> /// <returns>The bitmap witht the new size</returns> public static Bitmap Resize(Bitmap inputBitmap, int resizeFactor) { var resultBitmap = new Bitmap(inputBitmap.Width * resizeFactor, inputBitmap.Height * resizeFactor); using (var graphics = Graphics.FromImage(resultBitmap)) { graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.DrawImage(inputBitmap, new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height)); } return resultBitmap; } /// <summary> /// method that Highlights the countours of a Bitmap /// </summary> /// <param name=\"inputBitmap\">The bitmap you want to highlight the countours of</param> /// <returns>The bitmap with countours highlighted</returns> public static Bitmap HighlightContours(Bitmap inputBitmap) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); Bitmap grayscale = Grayscale(inputBitmap); Bitmap thresholded = Tresholding(grayscale, 128); Bitmap dilated = Dilatation(thresholded, 3); Bitmap eroded = Erode(dilated, 3); for (int y = 0; y < inputBitmap.Height; y++) { for (int x = 0; x < inputBitmap.Width; x++) { Color pixel = inputBitmap.GetPixel(x, y); Color dilatedPixel = dilated.GetPixel(x, y); Color erodedPixel = eroded.GetPixel(x, y); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); int threshold = dilatedPixel.R; if (gray > threshold) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } else if (gray <= threshold && erodedPixel.R == 0) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 0, 0)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } } } return outputBitmap; } /// <summary> /// Method that that erodes the morphology of a bitmap /// </summary> /// <param name=\"inputBitmap\">The bitmap you want to erode</param> /// <param name=\"kernelSize\">The amount of Erosion you want (be carefull its expensive on ressources)</param> /// <returns>The Bitmap with the eroded contents</returns> public static Bitmap Erode(Bitmap inputBitmap, int kernelSize) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); int[,] kernel = new int[kernelSize, kernelSize]; for (int i = 0; i < kernelSize; i++) { for (int j = 0; j < kernelSize; j++) { kernel[i, j] = 1; } } for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) { for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) { bool flag = true; for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) { for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) { Color pixel = inputBitmap.GetPixel(x + i, y + j); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); if (gray >= 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1) { flag = false; break; } } if (!flag) { break; } } if (flag) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } } } return outputBitmap; } /// <summary> /// Method that that use dilatation of the morphology of a bitmap /// </summary> /// <param name=\"inputBitmap\">The bitmap you want to use dilatation on</param> /// <param name=\"kernelSize\">The amount of dilatation you want (be carefull its expensive on ressources)</param> /// <returns>The Bitmap after Dilatation</returns> public static Bitmap Dilatation(Bitmap inputBitmap, int kernelSize) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); int[,] kernel = new int[kernelSize, kernelSize]; for (int i = 0; i < kernelSize; i++) { for (int j = 0; j < kernelSize; j++) { kernel[i, j] = 1; } } for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) { for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) { bool flag = false; for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) { for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) { Color pixel = inputBitmap.GetPixel(x + i, y + j); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); if (gray < 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1) { flag = true; break; } } if (flag) { break; } } if (flag) { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } } } return outputBitmap; } } }","title":"OcrImage.cs"},{"location":"Code/Settings.html","text":"Settings.cs \ufeffusing System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.IO; namespace Test_Merge { public partial class Settings : Form { private string _grandPrixUrl = \"\"; private string _grandPrixName = \"\"; private int _grandPrixYear = 2000; private List<string> _driverList = new List<string>(); private F1TVEmulator Emulator = null; private ConfigurationTool Config = null; private bool CreatingZone = false; private Point ZoneP1; private Point ZoneP2; private bool CreatingWindow = false; private Point WindowP1; private Point WindowP2; List<Rectangle> WindowsToAdd = new List<Rectangle>(); public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; } public string GrandPrixName { get => _grandPrixName; private set => _grandPrixName = value; } public int GrandPrixYear { get => _grandPrixYear; private set => _grandPrixYear = value; } public List<string> DriverList { get => _driverList; private set => _driverList = value; } public Settings() { InitializeComponent(); Load(); } private void Load() { RefreshUI(); } private void RefreshUI() { lsbDrivers.DataSource = null; lsbDrivers.DataSource = DriverList; if (Directory.Exists(ConfigurationTool.CONFIGS_FOLDER_NAME)) { lsbPresets.DataSource = null; lsbPresets.DataSource = Directory.GetFiles(ConfigurationTool.CONFIGS_FOLDER_NAME); } if (CreatingZone) { if (ZoneP1 == new Point(-1, -1)) { lblZonePointsRemaning.Text = \"2 points Remaining\"; } else { lblZonePointsRemaning.Text = \"1 point Remaining\"; } } else { lblZonePointsRemaning.Text = \"\"; } if (CreatingWindow) { if (WindowP1 == new Point(-1, -1)) { lblWindowPointsRemaining.Text = \"2 points Remaining\"; } else { lblWindowPointsRemaining.Text = \"1 point Remaining\"; } lblWindowPointsRemaining.Text = ConfigurationTool.NUMBER_OF_ZONES - WindowsToAdd.Count() + \" Windows remaining\"; } else { lblWindowPointsRemaining.Text = \"\"; lblWindowsRemaining.Text = \"\"; } if (Config != null) { pbxMain.Image = Config.MainZone.Draw(); if(Config.MainZone.Zones.Count > 0) pbxDriverZone.Image = Config.MainZone.Zones[0].Draw(); } } private void CreateNewZone(Point p1, Point p2) { Rectangle dimensions = CreateAbsoluteRectangle(p1, p2); Config = new ConfigurationTool((Bitmap)pbxMain.Image, dimensions); RefreshUI(); } private void CreateWindows(List<Rectangle> dimensions) { if (Config != null) { Config.AddWindows(dimensions); } } private void tbxGpUrl_TextChanged(object sender, EventArgs e) { GrandPrixUrl = tbxGpUrl.Text; } private void tbxGpName_TextChanged(object sender, EventArgs e) { GrandPrixName = tbxGpName.Text; } private void tbxGpYear_TextChanged(object sender, EventArgs e) { int year; try { year = Convert.ToInt32(tbxGpYear.Text); } catch { year = 1545; } GrandPrixYear = year; } private void btnAddDriver_Click(object sender, EventArgs e) { string newDriver = tbxDriverName.Text; DriverList.Add(newDriver); tbxDriverName.Text = \"\"; RefreshUI(); } private void btnRemoveDriver_Click(object sender, EventArgs e) { if (lsbDrivers.SelectedIndex >= 0) { DriverList.RemoveAt(lsbDrivers.SelectedIndex); } RefreshUI(); } private void SwitchZoneCreation() { if (CreatingZone) { CreatingZone = false; lblZonePointsRemaning.Text = \"\"; } else { CreatingZone = true; if (Config != null) Config.ResetMainZone(); if (CreatingWindow) SwitchWindowCreation(); if (Emulator != null && Emulator.Ready) { Config = null; pbxMain.Image = Emulator.Screenshot(); } ZoneP1 = new Point(-1, -1); ZoneP2 = new Point(-1, -1); lblZonePointsRemaning.Text = \"2 Points left\"; } RefreshUI(); } private void SwitchWindowCreation() { if (CreatingWindow) { CreatingWindow = false; } else { CreatingWindow = true; if (Config != null) Config.ResetWindows(); if (CreatingZone) SwitchZoneCreation(); WindowP1 = new Point(-1, -1); WindowP2 = new Point(-1, -1); WindowsToAdd = new List<Rectangle>(); } RefreshUI(); } private void btnCreatZone_Click(object sender, EventArgs e) { SwitchZoneCreation(); } private void btnCreateWindow_Click(object sender, EventArgs e) { SwitchWindowCreation(); } private void pbxMain_MouseClick(object sender, MouseEventArgs e) { if (CreatingZone && pbxMain.Image != null) { //Point coordinates = pbxMain.PointToClient(new Point(MousePosition.X, MousePosition.Y)); Point coordinates = e.Location; float xOffset = (float)pbxMain.Image.Width / (float)pbxMain.Width; float yOffset = (float)pbxMain.Image.Height / (float)pbxMain.Height; Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset)); //MessageBox.Show(\"Coordinates\" + Environment.NewLine + \"Old : \" + coordinates.ToString() + Environment.NewLine + \"New : \" + newPoint.ToString()); if (ZoneP1 == new Point(-1, -1)) { ZoneP1 = newPoint; } else { ZoneP2 = newPoint; CreateNewZone(ZoneP1, ZoneP2); SwitchZoneCreation(); } RefreshUI(); } } private void pbxMain_Click(object sender, EventArgs e) { //Not the right one to use visibly } private void pbxDriverZone_MouseClick(object sender, MouseEventArgs e) { if (CreatingWindow && pbxDriverZone.Image != null) { Point coordinates = e.Location; float xOffset = (float)pbxDriverZone.Image.Width / (float)pbxDriverZone.Width; float yOffset = (float)pbxDriverZone.Image.Height / (float)pbxDriverZone.Height; Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset)); if (WindowP1 == new Point(-1, -1)) { WindowP1 = newPoint; } else { WindowP2 = newPoint; WindowsToAdd.Add(CreateAbsoluteRectangle(WindowP1, WindowP2)); if (WindowsToAdd.Count < ConfigurationTool.NUMBER_OF_ZONES) { WindowP1 = new Point(-1, -1); WindowP2 = new Point(-1, -1); } else { WindowP1 = new Point(WindowP1.X, 0); WindowP2 = new Point(WindowP2.X, pbxDriverZone.Image.Height); CreateWindows(WindowsToAdd); SwitchWindowCreation(); } } RefreshUI(); } } private void pbxDriverZone_Click(object sender, EventArgs e) { //Not the right one to use visibly } private Rectangle CreateAbsoluteRectangle(Point p1, Point p2) { Point newP1 = new Point(); Point newP2 = new Point(); if (p1.X < p2.X) { newP1.X = p1.X; newP2.X = p2.X; } else { newP1.X = p2.X; newP2.X = p1.X; } if (p1.Y < p2.Y) { newP1.Y = p1.Y; newP2.Y = p2.Y; } else { newP1.Y = p2.Y; newP2.Y = p1.Y; } return new Rectangle(newP1.X, newP1.Y, newP2.X - newP1.X, newP2.Y - newP1.Y); } private async void btnRefresh_Click(object sender, EventArgs e) { btnRefresh.Enabled = false; if (Emulator == null || Emulator.GrandPrixUrl != tbxGpUrl.Text) { Emulator = new F1TVEmulator(tbxGpUrl.Text); } if (!Emulator.Ready) { Task<int> start = Task.Run(() => Emulator.Start()); int errorCode = await start; if (errorCode != 0) { string message; switch (errorCode) { case 101: message = \"Error \" + errorCode + \" Could not start the driver. It could be because an other instance is runnin make sure you closed them all before trying again\"; break; case 102: message = \"Error \" + errorCode + \" Could not navigate on the F1TV site. Make sure the correct URL has been given and that you logged from chrome. It can take a few minutes to update\"; break; case 103: message = \"Error \" + errorCode + \" The url is not a valid url\"; break; case 104: message = \"Error \" + errorCode + \" The url is not a valid url\"; break; case 105: message = \"Error \" + errorCode + \" There has been an error trying to emulate button presses. Please try again\"; break; case 106: message = \"Error \" + errorCode + \" There has been an error trying to emulate button presses. Please try again\"; break; default: message = \"Could not start the emulator Error \" + errorCode; break; } MessageBox.Show(message); } else { pbxMain.Image = Emulator.Screenshot(); } } else { pbxMain.Image = Emulator.Screenshot(); } btnRefresh.Enabled = true; } private void Settings_FormClosing(object sender, FormClosingEventArgs e) { if (Emulator != null) { Emulator.Stop(); } } private void btnResetDriver_Click(object sender, EventArgs e) { if (Emulator != null) { Emulator.ResetDriver(); } } private void btnSavePreset_Click(object sender, EventArgs e) { string presetName = tbxPresetName.Text; if (Config != null) { Config.SaveToJson(DriverList,presetName); } RefreshUI(); } private void lsbPresets_SelectedIndexChanged(object sender, EventArgs e) { //Nothing } private void btnLoadPreset_Click(object sender, EventArgs e) { if (lsbPresets.SelectedIndex >= 0 && pbxMain.Image != null) { try { Reader reader = new Reader(lsbPresets.Items[lsbPresets.SelectedIndex].ToString(), (Bitmap)pbxMain.Image,false); //MainZones #0 is the big main zone containing driver zones Config = new ConfigurationTool((Bitmap)pbxMain.Image, reader.MainZones[0].Bounds); Config.MainZone = reader.MainZones[0]; DriverList = reader.Drivers; } catch (Exception ex) { MessageBox.Show(\"Could not load the settings error :\" + ex); } RefreshUI(); } } } }","title":"Settings.cs"},{"location":"Code/Settings.html#settingscs","text":"\ufeffusing System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.IO; namespace Test_Merge { public partial class Settings : Form { private string _grandPrixUrl = \"\"; private string _grandPrixName = \"\"; private int _grandPrixYear = 2000; private List<string> _driverList = new List<string>(); private F1TVEmulator Emulator = null; private ConfigurationTool Config = null; private bool CreatingZone = false; private Point ZoneP1; private Point ZoneP2; private bool CreatingWindow = false; private Point WindowP1; private Point WindowP2; List<Rectangle> WindowsToAdd = new List<Rectangle>(); public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; } public string GrandPrixName { get => _grandPrixName; private set => _grandPrixName = value; } public int GrandPrixYear { get => _grandPrixYear; private set => _grandPrixYear = value; } public List<string> DriverList { get => _driverList; private set => _driverList = value; } public Settings() { InitializeComponent(); Load(); } private void Load() { RefreshUI(); } private void RefreshUI() { lsbDrivers.DataSource = null; lsbDrivers.DataSource = DriverList; if (Directory.Exists(ConfigurationTool.CONFIGS_FOLDER_NAME)) { lsbPresets.DataSource = null; lsbPresets.DataSource = Directory.GetFiles(ConfigurationTool.CONFIGS_FOLDER_NAME); } if (CreatingZone) { if (ZoneP1 == new Point(-1, -1)) { lblZonePointsRemaning.Text = \"2 points Remaining\"; } else { lblZonePointsRemaning.Text = \"1 point Remaining\"; } } else { lblZonePointsRemaning.Text = \"\"; } if (CreatingWindow) { if (WindowP1 == new Point(-1, -1)) { lblWindowPointsRemaining.Text = \"2 points Remaining\"; } else { lblWindowPointsRemaining.Text = \"1 point Remaining\"; } lblWindowPointsRemaining.Text = ConfigurationTool.NUMBER_OF_ZONES - WindowsToAdd.Count() + \" Windows remaining\"; } else { lblWindowPointsRemaining.Text = \"\"; lblWindowsRemaining.Text = \"\"; } if (Config != null) { pbxMain.Image = Config.MainZone.Draw(); if(Config.MainZone.Zones.Count > 0) pbxDriverZone.Image = Config.MainZone.Zones[0].Draw(); } } private void CreateNewZone(Point p1, Point p2) { Rectangle dimensions = CreateAbsoluteRectangle(p1, p2); Config = new ConfigurationTool((Bitmap)pbxMain.Image, dimensions); RefreshUI(); } private void CreateWindows(List<Rectangle> dimensions) { if (Config != null) { Config.AddWindows(dimensions); } } private void tbxGpUrl_TextChanged(object sender, EventArgs e) { GrandPrixUrl = tbxGpUrl.Text; } private void tbxGpName_TextChanged(object sender, EventArgs e) { GrandPrixName = tbxGpName.Text; } private void tbxGpYear_TextChanged(object sender, EventArgs e) { int year; try { year = Convert.ToInt32(tbxGpYear.Text); } catch { year = 1545; } GrandPrixYear = year; } private void btnAddDriver_Click(object sender, EventArgs e) { string newDriver = tbxDriverName.Text; DriverList.Add(newDriver); tbxDriverName.Text = \"\"; RefreshUI(); } private void btnRemoveDriver_Click(object sender, EventArgs e) { if (lsbDrivers.SelectedIndex >= 0) { DriverList.RemoveAt(lsbDrivers.SelectedIndex); } RefreshUI(); } private void SwitchZoneCreation() { if (CreatingZone) { CreatingZone = false; lblZonePointsRemaning.Text = \"\"; } else { CreatingZone = true; if (Config != null) Config.ResetMainZone(); if (CreatingWindow) SwitchWindowCreation(); if (Emulator != null && Emulator.Ready) { Config = null; pbxMain.Image = Emulator.Screenshot(); } ZoneP1 = new Point(-1, -1); ZoneP2 = new Point(-1, -1); lblZonePointsRemaning.Text = \"2 Points left\"; } RefreshUI(); } private void SwitchWindowCreation() { if (CreatingWindow) { CreatingWindow = false; } else { CreatingWindow = true; if (Config != null) Config.ResetWindows(); if (CreatingZone) SwitchZoneCreation(); WindowP1 = new Point(-1, -1); WindowP2 = new Point(-1, -1); WindowsToAdd = new List<Rectangle>(); } RefreshUI(); } private void btnCreatZone_Click(object sender, EventArgs e) { SwitchZoneCreation(); } private void btnCreateWindow_Click(object sender, EventArgs e) { SwitchWindowCreation(); } private void pbxMain_MouseClick(object sender, MouseEventArgs e) { if (CreatingZone && pbxMain.Image != null) { //Point coordinates = pbxMain.PointToClient(new Point(MousePosition.X, MousePosition.Y)); Point coordinates = e.Location; float xOffset = (float)pbxMain.Image.Width / (float)pbxMain.Width; float yOffset = (float)pbxMain.Image.Height / (float)pbxMain.Height; Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset)); //MessageBox.Show(\"Coordinates\" + Environment.NewLine + \"Old : \" + coordinates.ToString() + Environment.NewLine + \"New : \" + newPoint.ToString()); if (ZoneP1 == new Point(-1, -1)) { ZoneP1 = newPoint; } else { ZoneP2 = newPoint; CreateNewZone(ZoneP1, ZoneP2); SwitchZoneCreation(); } RefreshUI(); } } private void pbxMain_Click(object sender, EventArgs e) { //Not the right one to use visibly } private void pbxDriverZone_MouseClick(object sender, MouseEventArgs e) { if (CreatingWindow && pbxDriverZone.Image != null) { Point coordinates = e.Location; float xOffset = (float)pbxDriverZone.Image.Width / (float)pbxDriverZone.Width; float yOffset = (float)pbxDriverZone.Image.Height / (float)pbxDriverZone.Height; Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset)); if (WindowP1 == new Point(-1, -1)) { WindowP1 = newPoint; } else { WindowP2 = newPoint; WindowsToAdd.Add(CreateAbsoluteRectangle(WindowP1, WindowP2)); if (WindowsToAdd.Count < ConfigurationTool.NUMBER_OF_ZONES) { WindowP1 = new Point(-1, -1); WindowP2 = new Point(-1, -1); } else { WindowP1 = new Point(WindowP1.X, 0); WindowP2 = new Point(WindowP2.X, pbxDriverZone.Image.Height); CreateWindows(WindowsToAdd); SwitchWindowCreation(); } } RefreshUI(); } } private void pbxDriverZone_Click(object sender, EventArgs e) { //Not the right one to use visibly } private Rectangle CreateAbsoluteRectangle(Point p1, Point p2) { Point newP1 = new Point(); Point newP2 = new Point(); if (p1.X < p2.X) { newP1.X = p1.X; newP2.X = p2.X; } else { newP1.X = p2.X; newP2.X = p1.X; } if (p1.Y < p2.Y) { newP1.Y = p1.Y; newP2.Y = p2.Y; } else { newP1.Y = p2.Y; newP2.Y = p1.Y; } return new Rectangle(newP1.X, newP1.Y, newP2.X - newP1.X, newP2.Y - newP1.Y); } private async void btnRefresh_Click(object sender, EventArgs e) { btnRefresh.Enabled = false; if (Emulator == null || Emulator.GrandPrixUrl != tbxGpUrl.Text) { Emulator = new F1TVEmulator(tbxGpUrl.Text); } if (!Emulator.Ready) { Task<int> start = Task.Run(() => Emulator.Start()); int errorCode = await start; if (errorCode != 0) { string message; switch (errorCode) { case 101: message = \"Error \" + errorCode + \" Could not start the driver. It could be because an other instance is runnin make sure you closed them all before trying again\"; break; case 102: message = \"Error \" + errorCode + \" Could not navigate on the F1TV site. Make sure the correct URL has been given and that you logged from chrome. It can take a few minutes to update\"; break; case 103: message = \"Error \" + errorCode + \" The url is not a valid url\"; break; case 104: message = \"Error \" + errorCode + \" The url is not a valid url\"; break; case 105: message = \"Error \" + errorCode + \" There has been an error trying to emulate button presses. Please try again\"; break; case 106: message = \"Error \" + errorCode + \" There has been an error trying to emulate button presses. Please try again\"; break; default: message = \"Could not start the emulator Error \" + errorCode; break; } MessageBox.Show(message); } else { pbxMain.Image = Emulator.Screenshot(); } } else { pbxMain.Image = Emulator.Screenshot(); } btnRefresh.Enabled = true; } private void Settings_FormClosing(object sender, FormClosingEventArgs e) { if (Emulator != null) { Emulator.Stop(); } } private void btnResetDriver_Click(object sender, EventArgs e) { if (Emulator != null) { Emulator.ResetDriver(); } } private void btnSavePreset_Click(object sender, EventArgs e) { string presetName = tbxPresetName.Text; if (Config != null) { Config.SaveToJson(DriverList,presetName); } RefreshUI(); } private void lsbPresets_SelectedIndexChanged(object sender, EventArgs e) { //Nothing } private void btnLoadPreset_Click(object sender, EventArgs e) { if (lsbPresets.SelectedIndex >= 0 && pbxMain.Image != null) { try { Reader reader = new Reader(lsbPresets.Items[lsbPresets.SelectedIndex].ToString(), (Bitmap)pbxMain.Image,false); //MainZones #0 is the big main zone containing driver zones Config = new ConfigurationTool((Bitmap)pbxMain.Image, reader.MainZones[0].Bounds); Config.MainZone = reader.MainZones[0]; DriverList = reader.Drivers; } catch (Exception ex) { MessageBox.Show(\"Could not load the settings error :\" + ex); } RefreshUI(); } } } }","title":"Settings.cs"},{"location":"Code/recoverCookiesCSV.html","text":"recoverCookiesCSV.py # Rohmer Maxime # RecoverCookies.py # Little script that recovers the cookies stored in the chrome sqlite database and then decrypts them using the key stored in the chrome files # This script has been created to be used by an other programm or for the data to not be used directly. This is why it stores all the decoded cookies in a csv. (Btw could be smart for the end programm to delete the csv after using it) # Parts of this cript have been created with the help of ChatGPT import os import json import base64 import sqlite3 import win32crypt from Cryptodome.Cipher import AES from pathlib import Path import csv 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\")","title":"recoverCookiesCSV.py"},{"location":"Code/recoverCookiesCSV.html#recovercookiescsvpy","text":"# Rohmer Maxime # RecoverCookies.py # Little script that recovers the cookies stored in the chrome sqlite database and then decrypts them using the key stored in the chrome files # This script has been created to be used by an other programm or for the data to not be used directly. This is why it stores all the decoded cookies in a csv. (Btw could be smart for the end programm to delete the csv after using it) # Parts of this cript have been created with the help of ChatGPT import os import json import base64 import sqlite3 import win32crypt from Cryptodome.Cipher import AES from pathlib import Path import csv 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\")","title":"recoverCookiesCSV.py"}]}