Files
TrackTrendsDoc/site/search/search_index.json
T
2023-06-09 09:33:05 +02:00

1 line
1.1 MiB
Plaintext

{"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. It's 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 lap times 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 useful 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 rubbish) 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 : \"Prototype 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 Ici, je vais parler de l'\u00e9tat du projet \u00e0 la date du 12 Juin 2023. \u00c0 cette date, le projet est fonctionnel, mais comporte quelques diff\u00e9rences avec le cahier des charges original. Je vais expliquer non seulement ces diff\u00e9rences, mais aussi les raisons qui font qu'elles sont l\u00e0. Pour bien comprendre les diff\u00e9rences, il faut s'en r\u00e9f\u00e9rer au cahier des charges original. L'application doit \u00eatre \"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\". C'est \u00e7a la phrase la plus importante dans tout le CDC. Et je pense que tr\u00e8s honn\u00eatement, ce cahier des charges est rempli ! L'application actuellement disponible sur le repos GIT est une application de style compagnion Windows Forms qui r\u00e9cup\u00e8re les infos de la F1TV en temps r\u00e9el et elle affiche les informations qu'elle trouve importante. Donc, je dirais que l'objectif g\u00e9n\u00e9ral est rempli. Maintenant, c'est dans les d\u00e9tails que cela p\u00eache. Il est mentionn\u00e9 trois exemples d'infos \u00e0 suivre, je cite : \"Les pilotes qui sont proches (moins de 1-2 secondes qui sont ainsi 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\" r\u00e9sultats : Dans l'application, on peut effectivement voir les pilotes proches (Ce sont ceux qui sont \u00e0 moins de 3 secondes dans le version finale) Dans l'application, on peut aussi voir un affichage qui permet de voir les pilotes les plus rapides et les plus lents sur le circuit. On ne peut en revanche pas voir de classement pond\u00e9r\u00e9 selon les arr\u00eats aux stands, car l'application a du mal \u00e0 d\u00e9tecter des arr\u00eats. Ensuite pour ce qui est des pr\u00e9dictions, il n'y en a aucunes comme \u00e7a, c'est simple. Si on ne regarde que de tr\u00e8s loin le CDC et le projet final, on pourrait dire que c'est plut\u00f4t d\u00e9cevant puisqu'il manque beaucoup de choses comme les pr\u00e9dictions et certains affichages. On peut aussi se dire \u00e7a en comparant la maquette du CDC et le r\u00e9sultat final. \"Maquette originale du projet faite sur Figma\" \"Maquette originale du projet faite sur Figma\" Clairement, un \u0153il non avis\u00e9 pourrait \u00eatre tr\u00e8s d\u00e9\u00e7u et pourrait dire que c'est un \u00e9chec. Et moi je vais vous expliquer pourquoi, au contraire c'est un total succ\u00e8s. D\u00e9j\u00e0, la beaut\u00e9 de l'interface est tr\u00e8s difficile \u00e0 r\u00e9pliquer en Windows Forms et il faudrait plus d'une semaine de travail pour arriver \u00e0 quelque chose qui pourrait ressembler un tout petit peu \u00e0 la maquette. Ensuite, si on regarde bien, on a quand m\u00eame une application qui nous permet de suivre les informations de la course et qui calcule des choses \u00e0 notre place. C'est d\u00e9j\u00e0 une grosse plus-value par rapport \u00e0 la page Data de la F1TV. Et finalement, les pr\u00e9dictions, les affichages et le style, ce sont les choses les moins compliqu\u00e9es du projet. On ne se rend pas compte que pour simplement afficher les 20 pilotes dans le bon ordre, il faut \u00e9norm\u00e9ment de travail. Voici une petite repr\u00e9sentation graphique de la quantit\u00e9 de travail n\u00e9cessaire pour en arriver \u00e0 l'\u00e9tat actuel du projet : \"Graphique repr\u00e9sentant la quantit\u00e9 de travail requise\" Pour en arriver \u00e0 un affichage, il a fallu r\u00e9cup\u00e9rer automatiquement les images en utilisant un browser headless ce qui a pris un temps fou \u00e0 mettre en place et il a fallu surtout lire les informations que l'on recevait des images. J'ai pass\u00e9 presque 90\u2009% du temps de mon projet \u00e0 d\u00e9velopper des choses qui permettront ensuite de faire de l'affichage. Le fait qu'il y ait quoi que ce soit de logique qui s'affiche, cela veut dire que TOUT LE RESTE fonctionne ! Le moindre souci \u00e0 la r\u00e9cup\u00e9ration des images, ou surtout \u00e0 la reconnaissance de texte et de chiffres, et l'affichage est ruin\u00e9. Si j'avais pass\u00e9 ne serait-ce qu'une semaine de plus juste sur l'affichage, le r\u00e9sultat final n'aurait rien \u00e0 voir. Le souci, c'est simplement que le cahier des charges ne parle pas du tout du reste du projet et ne parle que du r\u00e9sultat final. Pour toutes ces raisons, je dirais que le CDC \u00e9tait trop superficiel, mais que l'application est conforme \u00e0 l'id\u00e9e g\u00e9n\u00e9rale de ce dernier et qu'il serait tr\u00e8s facile de la rendre parfaitement conforme maintenant que tout le travail de fond a \u00e9t\u00e9 fait et fonctionne et je pense donc que c'est un succ\u00e8s. 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, puisque 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 au niveau de la documentation et 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 peaufiner. 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 assur\u00e9ment 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 Alors ! Ces lignes sont \u00e9crites dans les derniers jours du travail de dipl\u00f4me et j'ai des choses \u00e0 dire. Premi\u00e8rement, je suis plut\u00f4t content de mon estimation du travail. Je trouve que j'ai bien estim\u00e9 la quantit\u00e9 de travail et combien de temps les diff\u00e9rentes t\u00e2ches allaient prendre. La plupart des d\u00e9passements sont des impr\u00e9vus et/ou des allers et des retours entre d'autres t\u00e2ches. La raison pour laquelle je suis plut\u00f4t content de ma planification, c'est que malgr\u00e9 l'usine \u00e0 Gaz que repr\u00e9sente ce projet et le nombre de soucis que j'ai eu, j'ai quand m\u00eame pu arriver \u00e0 un projet qui fonctionne en suivant essentiellement fid\u00e8lement le planning. Une chose dont je suis assez fier, c'est la documentation. En ayant d\u00e9velopp\u00e9 le squelette de l'app d\u00e8s le d\u00e9but du projet, \u00e7a m'a permis d'avancer au fur et \u00e0 mesure du projet la conscience tranquille. Bon, c'est bien joli les fleurs, mais clairement, c'est loin d'\u00eatre parfait. Au moment de la planification, je n'avais pas pr\u00e9vu de faire des allers et des retours entre plusieurs t\u00e2ches. Dans le planning effectif, on peut voir qu'un jour, je suis sur la PT3 (Stockage) et la PT5 (regroupement des mini projets en un seul gros). J'aurais peut-\u00eatre d\u00fb inverser l'ordre. Mais il y a deux gros soucis dans mon planning : L'ordre des t\u00e2ches n'\u00e9tait pas bon (mais il a \u00e9t\u00e9 d\u00e9cid\u00e9 comme \u00e7a pour que les plus grosses difficult\u00e9s soient faites en premier) ce qui a cr\u00e9\u00e9 pas mal de soucis. Ex : L'\u00e9mulateur de la F1TV a \u00e9t\u00e9 fait tr\u00e8s tard et finalement les images r\u00e9cup\u00e9r\u00e9es n'\u00e9taient pas de la m\u00eame qualit\u00e9 que ce que j'avais pr\u00e9vu en d\u00e9veloppant l'OCR en premier. Les Tests ont \u00e9t\u00e9 n\u00e9glig\u00e9s et utilis\u00e9s comme des jours tampons. \u00c7a, c'est la plus grosse erreur de planning. Autant les autres sont p\u00e9nibles, etc. mais n'ont pas forc\u00e9ment compromis la bonne r\u00e9alisation du projet alors que les tests ont \u00e9t\u00e9 mal plac\u00e9s et ont finalement \u00e9t\u00e9 balay\u00e9s tandis que s'ils avaient \u00e9t\u00e9 mieux planifi\u00e9s \u00e7a ne serait pas arriv\u00e9. Solutions : L'ordre des t\u00e2ches a \u00e9t\u00e9 d\u00e9cid\u00e9 expr\u00e8s de cette fa\u00e7on pour \u00e9viter de prendre trop de risques. L'id\u00e9e \u00e9tait qu'en faisant le plus dur au d\u00e9but, je pourrai facilement changer le cahier des charges. J'ai envie de dire que j'aurais d\u00fb \u00eatre plus confiant, mais pour \u00eatre honn\u00eate, je pense que c'\u00e9tait un mal pour un bien. Je ne pense pas avoir \"bien\" fait, mais je pense que c'est une erreur qui \u00e9tait rentable pour mon niveau de stress dans le projet. Par contre, les Tests c'est tout simplement une erreur. J'en parle plus en d\u00e9tail dans la partie test de la documentation, mais je vais r\u00e9sumer un peu ici. La documentation a \u00e9t\u00e9 faite d\u00e8s le d\u00e9but du projet. J'ai mis en place le squelette pour qu'ensuite, il soit simple d'y ajouter au fur et \u00e0 mesure. J'aurais d\u00fb faire exactement pareil avec les tests. Si j'avais fait au moins le squelette des tests au d\u00e9but du projet, j'aurais pu beaucoup plus facilement en faire et cela m'aurait fait gagner un temps fou et j'aurais m\u00eame pu faire du TDD (Test Driven Developpement). Je suis persuad\u00e9 que cette b\u00eate erreur de planification m'a co\u00fbt\u00e9 tr\u00e8s cher, car ne pas avoir une bonne strat\u00e9gie de tests a d\u00fb me faire perdre un temps fou. Pour conclure, je suis content parce que j'ai r\u00e9ussi \u00e0 rendre un projet qui marche en suivant assez bien le planning, mais il y a des choses que je vais devoir changer dans mes prochains projets. Analyse fonctionnelle Voir \"Manuel Utilisateur\" tout y est indiqu\u00e9 Analyse Organique Outils utilis\u00e9s Visual Studio 2022 \"Logo de Visual Studio 2022\" C'est l'application que j'ai le plus utilis\u00e9, je pense. Visual Studio 2022 est l'IDE officiel de Microsoft pour coder en C#. C'est l'outil que j'utilise depuis maintenant six ans au CFPT et franchement, il fait tout ce que je pourrais vouloir. C'est aussi un outil pratique pour utiliser Windows Forms et faire des applications natives Windows. Pas grand-chose \u00e0 dire \u00e0 ce sujet \u00e0 part que c'est un outil qui marche bien et qui est gratuit si on prend la community \u00e9dition. Visual Studio Code \"Logo de Visual Studio Code\" Cet outil est d\u00e9j\u00e0 un peu plus int\u00e9ressant. C'est le second outil que j'ai le plus utilis\u00e9. J'en ai surtout eu besoin pour \u00e9crire de la doc, mais aussi pour coder en python et pour contr\u00f4ler mkdocs. Visual Studio est un IDE absolument g\u00e9nial qui est tr\u00e8s puissant avec les bonnes extensions. Je l'utilise au quotidien pour tout ce qui est d\u00e9veloppement WEB, Mobile ou pour \u00e9diter des fichiers de configs pour mes drones ou imprimantes 3D. Je peux m\u00eame compiler le firmware pour ces derni\u00e8res en utilisant une extension faite pour. Les possibilit\u00e9s de customisation sont presque infinies et c'est un plaisir d'utiliser ce logiciel gratuit fourni par Microsoft, mais qui est am\u00e9lior\u00e9 constamment par des d\u00e9veloppeurs ind\u00e9pendants. Je conseille \u00e0 n'importe quel d\u00e9veloppeur de l'essayer \u00e0 moins qu'il soit uniquement sur C# ou il serait plus int\u00e9ressant d'utiliser visual studio 2022. Material/Mkdocs/Markdown \"Logo de Mkdocs Materials\" Pendant ce projet, j'ai utilis\u00e9 exclusivement du Markdown avec l'aide de Mkdocs et Materials. Le choix de Markdown a \u00e9t\u00e9 plut\u00f4t simple, c'est une fa\u00e7on facile et efficace de cr\u00e9er de la documentation et on n'avait pas le choix de l'utiliser. On avait \u00e9galement l'obligation (Ou au moins un tr\u00e8s forte incitation) par nos professeurs d'utiliser mkdocs et materials pour que notre documentation ne soit pas simplement une liste de fichiers, mais un joli site dans lequel il est agr\u00e9able de chercher des informations. Mkdocs et Materials sont deux outils vraiment fantastiques, mais je dois avouer que je n'ai pas assez mis de temps pour apprendre tout leur potentiel. Pour moi ce sont simplement des outils et je veux qu'ils marchent. Je ne suis pas forc\u00e9ment du genre \u00e0 aller changer toutes les couleurs et polices pour avoir la doc parfaite, j'ai pr\u00e9f\u00e9r\u00e9 passer du temps sur mon app. Mais m\u00eame si ces outils offrent une customisation tr\u00e8s avanc\u00e9e, il est tr\u00e8s facile de cr\u00e9er un projet simple et j'aime beaucoup cette simplicit\u00e9. J'ai eu pas mal d'aide de la part de M. Briard pour impl\u00e9menter certaines features et je l'en remercie tr\u00e8s chaudement, car sans son aide ce document serait s\u00fbrement un peu moins commode \u00e0 lire (Oui oui \u00e7a aurait pu \u00eatre pire, je sais, c'est dur \u00e0 imaginer). Figma \"Logo de Figma\" Figma est l'outil que j'ai utilis\u00e9 pour cr\u00e9er mon poster et un certain nombre des diagrammes de cette documentation. J'utilise aussi cet outil d\u00e8s que je vais faire des maquettes de sites ou d'applications. D'ailleurs les maquettes dans le cahier des charges ont \u00e9t\u00e9 faites avec. C'est un outil en ligne parfaitement gratuit qui conserve tout dans le cloud. Franchement, je n'ai rien \u00e0 dire, je n'ai pas utilis\u00e9 plus de 15\u2009% des features que cet outil propose et je suis d\u00e9j\u00e0 conquis. Technologies utilis\u00e9es Dans ce projet, diff\u00e9rents choix ont \u00e9t\u00e9 faits pour ce qui est des technologies. Certaines ont \u00e9t\u00e9 choisies, car elles \u00e9taient les plus simples, les plus pratiques, les plus efficaces ou encore les plus connues et donc ayant le meilleur support. Je vais tenter de r\u00e9sumer ici ces choix, mais je reviendrai sur la plupart d'entre eux plus tard quand j'explique ce que je fais avec. Selenium \"Logo de s\u00e9l\u00e9nium\" Selenium est une librairie \u00e0 la base Node JS qui permet d'automatiser des actions sur un navigateur internet. Le but premier et je pense son utilisation premi\u00e8re et l'automatisation de tests pour des applications WEB. En effet, c'est un super outil pour simuler un user faisant un certain nombre d'actions sans apporter de variabilit\u00e9, ce qui fait de super test unitaires. Cependant, je pense que l'autre grande partie des utilisateurs de Selenium l'utilisent pour faire du \"Scrapping\". Et nous sommes un peu dans cette seconde cat\u00e9gorie. Le \"Scrapping\" c'est l'acte d'aller r\u00e9cup\u00e9rer des informations sur des pages web automatiquement pour alimenter sa propre base de donn\u00e9es. En effet, si on arrive \u00e0 passer les protections anti-bot, on peut facilement utiliser Selenium pour scraper tous les sites qui nous passent par la t\u00eate. Le cahier des charges que j'avais en t\u00eate en cherchant une technologie de contr\u00f4le de navigateur internet \u00e9tait le suivant : Simple Permettant de contr\u00f4ler un navigateur Headless (Voir chapitre \"Simuler un navigateur ?\") Permettant de contr\u00f4ler Firefox Ayant un wrapper C# Permettre de changer certaines choses comme les cookies en direct Permettre d'interagir avec les \u00e9l\u00e9ments d'une page Fonctionner Simple, car je ne voulais pas avoir \u00e0 passer trop de temps dessus (\u00e7a n'a pas bien vieilli lol...). Je voulais que l'on puisse utiliser Firefox parce qu'il n'impl\u00e9mente pas les m\u00eames s\u00e9curit\u00e9s que Chrome pour faire simple. J'avais besoin que la lib puisse contr\u00f4ler un Firefox HEADLESS comme je ne voulais pas avoir une page web ouverte sur mon ordi quand je commente, car c'est de l'espace utilis\u00e9 pour rien. J'avais besoin d'un wrapper C# puisque c'est le langage que j'utilise. Pour finir, j'avais besoin d'interagir avec les \u00e9l\u00e9ments de la page pour naviguer dessus et d'ins\u00e9rer des cookies pour me connecter sans avoir \u00e0 passer par le login de la F1TV qui est tr\u00e8s bon pour d\u00e9tecter les bots. Avec un cahier des charges pareil beaucoup de librairies ont \u00e9t\u00e9 abandonn\u00e9es. J'ai pu tester pleins de librairies C# qui arrivaient \u00e0 contr\u00f4ler un Chrome et m\u00eame pas mal qui arrivaient \u00e0 contr\u00f4ler un Chrome Headless. Mais le choix est tr\u00e8s vite restreint quand on veut pouvoir contr\u00f4ler Chrome OU Firefox. \u00c0 la base, mon choix, c'\u00e9tait port\u00e9 sur Puppeteer Sharp qui est une librairie qui se veut \u00eatre exactement ce que je veux. \"Logo de Pupeteer\" Je voulais utiliser cette librairie, car il y a des plugins qui sont tr\u00e8s orient\u00e9s scrapping, en effet, ils impl\u00e9mentent de nombreuses techniques pour permettre de mieux passer inaper\u00e7u par les syst\u00e8mes de d\u00e9tection de bots. Sur le papier, c'est la librairie parfaite qui correspond parfaitement au cahier des charges que je m'\u00e9tais fix\u00e9 et je pense que si j'utilisais un projet JS, elle le serait. Sauf qu'avec le wrapper C# j'ai eu un certain nombre de probl\u00e8mes : Toutes les versions de la librairie ne fonctionnaient pas. Il fallait faire des tests avec diff\u00e9rentes versions de la librairie et de ses d\u00e9pendances simplement pour faire lancer un browser. Et \u00e7a, c'est quand \u00e7a marchait, car il y avait des jours o\u00f9 des machines sur lesquelles je n'ai juste pas pu faire fonctionner la librairie. M\u00eame avec les techniques propos\u00e9es par les plugins \"Stealth\" je n'arrivais pas \u00e0 bypass les s\u00e9curit\u00e9s de la page de login de la F1TV. J'ai essay\u00e9 tout ce que j'ai pu trouver sur internet, mais on se fait toujours chopper d\u00e8s que l'on arrive sur la page. Et le pire de tous, impossible de faire fonctionner une vid\u00e9o. J'ai pu faire tout ce que je voulais faire finalement en passant par l'utillisation de cookies pour la connexion. Tout \u00e7a pour arriver au moment o\u00f9 il faut lancer la vid\u00e9o, et l\u00e0, crash. Impossible de faire fonctionner Puppeteer Sharp avec une vid\u00e9o. D\u00e8s qu'elle se lance, c'est un crash assur\u00e9 sans message d'erreur clair. Et le souci, c'est que le wrapper C# n'est pas vraiment bien support\u00e9 et que si c'est un bug de la lib, je ne risque pas de voir de fix avant un moment si ce n'est jamais. Pour toutes ces raisons, j'ai d\u00fb abandonner cette librairie, ce qui a \u00e9t\u00e9 tr\u00e8s dur, car j'avais pass\u00e9 beaucoup de temps dessus \u00e0 essayer de la faire marcher. Ensuite le choix de Selenium \u00e9tait plut\u00f4t simple, c'\u00e9tait la seule option restante. \u00c0 ce jour, je ne connais aucune autre librairie que Puppeteer ou Selenium qui puisse contr\u00f4ler un Firefox Headless en respectant mon cahier des charges et qui soit donc disponible depuis C#. Si je n'arrivais pas \u00e0 faire fonctionner Selenium, j'aurais d\u00fb abandonner l'id\u00e9e de simuler un navigateur tout simplement. Mais j'ai eu la chance que cette librairie fasse tout ce que je pouvais demander. C'est une super lib et m\u00eame si la version C# n'est vraiment pas bien document\u00e9e, la plupart des documentations de la version JS sont pertinentes pour la version C# m\u00eame si \u00e7a n'est pas la m\u00eame syntaxe. Pour r\u00e9sumer, j'ai choisi Puppeteer car c'\u00e9tait la seule option viable pour mon besoin. (Note : Par contre si je trouve la personne chez Mozilla ou puppeteer qui a d\u00e9cid\u00e9 d'hard coder la r\u00e9solution maximale du browser Headless que l'on peut override UNIQUEMENT en changeant les variables d'environnement de la machine ET DE NE LE DOCUMENTER QUASI NULLE PART JE JURE QUE CA VA TR\u00c8S MAL SE PASSER) CSharp \"Logo C#\" Je pense que c'est le choix le plus simple \u00e0 expliquer. C# est un langage de programmation orient\u00e9 objet relativement haut niveau qui a \u00e9t\u00e9 cr\u00e9\u00e9 par Microsoft et qui a comme cible le d\u00e9veloppement d'applications pour Windows. (On peut \u00e9videmment trouver des adaptations pour le faire tourner sur Linux, mais ce n'est pas vraiment le but du langage) En plus d'\u00eatre un superbe langage de programmation, c'est le langage que l'on apprend au CFPT informatique. C'est donc un langage avec lequel je suis beaucoup plus \u00e0 l'aise que pour d'autres langages comme le Python ou le JS. Mon but n'\u00e9tait pas de faire une application Web et je travaille sous Windows. Je savais que mon projet allait demander un minimum de programmation orient\u00e9e objet. J'ai ainsi imm\u00e9diatement pens\u00e9 \u00e0 utiliser C#. Cependant, j'aurais tr\u00e8s bien pu utiliser un langage comme python qui m'aurait clairement facilit\u00e9 la t\u00e2che avec des librairies bien plus fournies et plus souvent mises \u00e0 jour. Mais comme je ne suis pas du tout aussi \u00e0 l'aise avec, je pense que le C# \u00e9tait la meilleure option. Mes seuls regrets apr\u00e8s coup sont que je trouve les Windows Forms tr\u00e8s moches et qu'il est particuli\u00e8rement difficile de les rendre plus jolies et que les librairies disponibles en C# pour des sc\u00e9narios tr\u00e8s pr\u00e9cis ne sont pas au niveau de celles pour JS et pour Python. Cependant, si j'avais \u00e0 refaire le projet, je reprendrais C# je pense. Python\u202f? \"Logo Python\" Alors ce choix-l\u00e0 est plus compliqu\u00e9 \u00e0 comprendre. Pour tout le projet, j'ai tent\u00e9 de garder le C# comme langage et de ne pas utiliser autre chose. Cependant, j'ai d\u00fb utiliser une seule fois le Python dans un cas tr\u00e8s pr\u00e9cis. Je n'aime vraiment pas coder en python de base et clairement, j'aurais pr\u00e9f\u00e9r\u00e9 ne pas l'utiliser, mais je n'avais pas le choix. Le besoin dans le cas du python \u00e9tait le suivant : J'avais besoin d'un moyen de r\u00e9cup\u00e9rer des strings et les d\u00e9coder avec une cl\u00e9 encod\u00e9e avec le syst\u00e8me propri\u00e9taire de Windows d'encodage. Le souci, c'est que j'avais avec le C# c'est que les m\u00e9thodes de d\u00e9cryptions ne fonctionnent pas pareil qu'en python et tous les exemples que je pouvais trouver \u00e9taient en python. J'ai essay\u00e9 pendant un sacr\u00e9 moment de faire fonctionner la d\u00e9cryptions en C# mais sans succ\u00e8s. J'ai donc directement utilis\u00e9 le python comme faisait toutes les personnes que je pouvais voir sur internet et je pense que \u00e7a n'est pas une mauvaise id\u00e9e. En effet, cela veut dire que si \u00e0 un moment Chrome est mis \u00e0 jour, je n'aurai pas besoin d'aller ouvrir tout le code source de mon projet pour tout recompiler, j'aurai simplement besoin de changer ce script. Un des avantages du Python est quand m\u00eame qu'il y a beaucoup plus de gens qui codent dessus, et pour ce genre d'utilisation tr\u00e8s sp\u00e9cifique, c'est plut\u00f4t pratique. Le seul probl\u00e8me, c'est que cela oblige l'utilisateur \u00e0 avoir python install\u00e9 sur sa machine et que sa version doit \u00eatre compatible... (les joies de python). Firefox \"Logo Firefox Headless\" J'en parle d\u00e9j\u00e0 plus bas, mais le choix de navigateur est super important. D\u00e9j\u00e0 tous les navigateurs n'ont pas un mode Headless (sans t\u00eate, mieux expliqu\u00e9 dans la rubrique \"Simuler un navigateur ?\"). Par exemple, m\u00eame si Edge est maintenant bas\u00e9 sur Chromium, il n'existe pas de moyen de le faire tourner en Headless pour le moment. Autre souci, les librairies d'automatisation ne supportent pas tous les navigateurs. Par exemple, beaucoup supportent chrome, mais tr\u00e8s peu supportent Firefox ou Edge. Donc, il me fallait un navigateur qui puisse op\u00e9rer en Headless et qui soit support\u00e9 par plusieurs librairies d'automatisation. Il n'y a que Firefox et Chrome qui soient conformes \u00e0 ces exigences. (Je n'ai pas v\u00e9rifi\u00e9 pour TOUS les navigateurs. Peut-\u00eatre que les Opera GX ont aussi un mode headless super, mais je me suis concentr\u00e9 sur les navigateurs plus grand public). Chrome est support\u00e9 par plus de lib, mais le souci c'est que la F1TV utilise un lecteur de vid\u00e9o avec DRM (Plus d'infos l\u00e0-dessus dans la partie \"Simuler un navigateur\u202f?\") et donc le choix \u00e9tait simple. Il ne restait que Firefox. Tesseract Je pense que le choix le plus simple apr\u00e8s le C# fut l'utilisation de Tesseract. C'est tout simplement l'outil le plus utilis\u00e9 pour faire de l'OCR. \u00c0 la base, c'est une lib Python (Ah tiens encore ?) qui peut \u00eatre redoutablement efficace avec le bon dataset. Il existe d'autres outils, mais j'ai d\u00e9cid\u00e9 de prendre celui-l\u00e0 \u00e0 cause de son support juste incroyable et de son omnipr\u00e9sence dans la documentation OCR. En plus il est facile \u00e0 utiliser et je ne pense pas encore avoir fait le tour de tout son potentiel dans ce projet. Fonctionnement g\u00e9n\u00e9ral Avant de passer \u00e0 l'explication de chaque partie du projet en d\u00e9tail, je pense qu'il est important de faire un petit point sur comment toutes les parties du projet s'emboitent et fonctionnement ensemble. Comme \u00e7a, quand vous lirez l'explication d'une \u00e9tape, vous serez conscient de \u00e0 quoi elle sert, et o\u00f9 elle s'inscrit dans le projet principal. Les briques principales Voici trois grosses \u00e9tapes du projet. Pour rappel, ce sont des vulgarisations plut\u00f4t larges qui n'ont qu'un seul but, aider \u00e0 la compr\u00e9hension de ce qui vient par la suite. R\u00e9cup\u00e9ration d'images \"Diagramme simplifi\u00e9 repr\u00e9sentant le processus de r\u00e9cup\u00e9ration des images\" Pour faire simple, on peut voir qu'il y a deux parties \u00e0 cette \u00e9tape. La premi\u00e8re en partant du haut repr\u00e9sente un script python qui va chercher des informations dans la base de donn\u00e9es de Chrome qui est en SQLite. Ces informations dans notre cas sont les cookies de connexion. Dans la seconde \u00e9tape, on peut voir que le programme utilise Selenium avec un navigateur Firefox Headless qui va aller communiquer avec la F1TV qui est le site web qui nous int\u00e9resse et qu'une des infos que l'on r\u00e9cup\u00e8re est une image de la page en format PNG que l'on envoie au programme C#. Ces deux parties sont li\u00e9es, car pour se connecter \u00e0 la F1TV Selenium a besoin des cookies de connexion r\u00e9cup\u00e9r\u00e9s par le programme Python. La premi\u00e8re partie est un processus qui n'est utilis\u00e9 qu'une seule fois au d\u00e9marrage tandis que la r\u00e9cup\u00e9ration d'images et en continu pendant toute la dur\u00e9e de l'utilisation de l'application. OCR \"Diagramme simplifi\u00e9 repr\u00e9sentant le processus d'OCR\" ; On peut voir dans ce diagramme simplifi\u00e9 qu'avec l'aide de ce que contient le fichier \"Config.JSON\" on d\u00e9coupe l'image que l'on a r\u00e9cup\u00e9r\u00e9 au pr\u00e9alable en petits morceaux qui contiennent des informations. Ensuite, on prend cette image et on lui applique un filtre pour retirer le flou, la couleur, etc. Puis en utilisant de l'OCR (Optical Character Recognition) on en r\u00e9cup\u00e8re les informations sous forme de texte et on le renvoie dans le programme C# Dans cette partie explicative g\u00e9n\u00e9rale, on ne reviendra pas sur la cr\u00e9ation de ce fichier config. Pour plus d'infos \u00e0 son sujet, voir la rubrique (OCR/Fonctionnement g\u00e9n\u00e9ral) Traitement et affichage \"Diagramme simplifi\u00e9 repr\u00e9sentant le processus de traitement et d'affichage\" On peut voir dans ce dernier mini diagramme simplifi\u00e9 qu'on prend les donn\u00e9es que l'on r\u00e9cup\u00e9rait de l'\u00e9tape pr\u00e9c\u00e9dente qui ne sont pas forc\u00e9ment toutes coh\u00e9rentes et qu'on les traite pour leur redonner du sens avant de les stocker dans une base de donn\u00e9es SQLITE. Ensuite cette m\u00eame base de donn\u00e9e fournis les infos n\u00e9cessaires pour des affichages (Ces affichages sont directement r\u00e9cup\u00e9r\u00e9s depuis le projet en cours de fonctionnement). R\u00e9sum\u00e9 du fonctionnement g\u00e9n\u00e9ral \"Diagramme simplifi\u00e9 repr\u00e9sentant le processus global du projet\" Ce dernier diagramme est un sch\u00e9ma fait pour repr\u00e9senter de la mani\u00e8re la plus simple possible toutes les briques du projet et comment elles s'imbriquent ensemble. La repr\u00e9sentation est un peu diff\u00e9rente des trois autres diagrammes, car le but ici est surtout de montrer le chemin que fait la donn\u00e9e \u00e0 travers les couches. Toutes les parties du projet ne sont pas incluses, notamment la partie calibration dans un objectif de simplifier la lecture. Dans l'ordre, on peut voir que pour la partie r\u00e9cup\u00e9ration d'images, le python va r\u00e9cup\u00e9rer les cookies dans la base de donn\u00e9es chrome pour ensuite les retourner \u00e0 Selenium. Selenium va ensuite pouvoir lancer un navigateur (en l'occurrence Firefox) et utiliser les cookies r\u00e9cup\u00e9r\u00e9s pour aller sur la page de la F1TV qui va retourner un certain nombre d'infos \u00e0 Selenium. L'info qui nous int\u00e9resse le plus depuis Selenium ce sont les images de la page data de la F1Tv et ce sont elles que l'on va envoyer dans la partie LOAD du diagramme. Dans cette partie, on prend l'image de la F1TV et on la d\u00e9coupe selon les directives donn\u00e9es par le fichier Config.JSON (il renseigne les zones \u00e0 d\u00e9couper et ce qu'elles repr\u00e9sentent) et apr\u00e8s le d\u00e9coupage, on se retrouve avec une zone principale, vingt zones de pilotes et 9 fen\u00eatres de donn\u00e9es par zone de pilote donc 180 fen\u00eatres en tout. Ces fen\u00eatres sont ensuite envoy\u00e9es pour \u00eatre filtr\u00e9es (retirer le flou, mettre en \u00e9vidence les caract\u00e8res, en gros les pr\u00e9parer pour la reconnaissance) dans la partie OCR Dans cette partie, apr\u00e8s avoir filtr\u00e9 les images, on les envoie \u00e0 Tesseract pour qu'il nous retourne des r\u00e9sultats d'OCR. Ces r\u00e9sultats sont ce que Tesseract a trouv\u00e9 sur les images et ils sont retourn\u00e9s sous la forme de Data Pilote. Ex (Position : 1,Tour : 45, Temps au tour : 1:34.683, Pneus : Medium etc....) Finalement, ces donn\u00e9es ont envoy\u00e9es dans la partie traitement qui va faire des v\u00e9rifications d'usage pour s'assurer qu'elles sont correctes et quand c'est fait, tout est envoy\u00e9 dans une base de donn\u00e9es SQLite. On ne montre pas non plus dans ce diagramme la parte affichage des donn\u00e9es, car je ne trouve pas pertinent de l'inclure ici. Et voil\u00e0, c'est le fonctionnement tr\u00e8s g\u00e9n\u00e9ral et simplifi\u00e9 de l'application. Je vous invite \u00e0 continuer \u00e0 lire cette documentation pour des informations plus pr\u00e9cises \u00e0 propos de toutes ces \u00e9tapes. Bonne lecture ! 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 mainmise sur les donn\u00e9es, ils peuvent ins\u00e9rer 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 Compagnon, mais bon) et c'est, je pense, la raison pour laquelle on ne voit aucune API publique qui permette de correctement se renseigner en donn\u00e9es en direct pendant un Grand Prix. Ils ont d\u00fb 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\" \u00c9videmment, 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 suffisantes et je ne peux pas leur faire confiance quand je commente. Ce qu'il m'aurait fallu, c'est une API publique et officielle qui me permette d'\u00eatre s\u00fbr 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\u00e8tement 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 juste 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 faciles \u00e0 avoir qu'elles sont IMPOSSIBLE \u00e0 avoir. Et c'est l\u00e0 que ce projet entre en jeu. Mais pour d\u00e9coder les donn\u00e9es d'une image, il faut d'abord ... (roulement de tambours) ... Avoir des images ! Et c'est l\u00e0 que vient se glisser cette partie du projet. Comment faire ? Le but de ce segment est de se concentrer sur la r\u00e9cup\u00e9ration 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\u00e9der directement au flux sans passer par la plateforme internet et pouvoir prendre images \u00e0 volont\u00e9. Avoir tout simplement une page de la F1TV ouverte sur un \u00e9cran et prendre des screenshots \u00e0 intervalles 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\u00e9r\u00e9es de la m\u00eame mani\u00e8re que les diffusions en direct. Et que pour faire des Tests en direct, 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 peut 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 fa\u00e7on, 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\u00e8mes et permet une r\u00e9cup\u00e9ration quasi sans compromis. Simuler un navigateur ? \"Navigateur Headless (sans t\u00eate)\" 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 (Interface Utilisateur). \"Chromium logo\" Cependant, La F1TV n'utilise pas simplement un player HTML5 basique. Elle utilise un service de streaming Bit Movin 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 versions 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 fa\u00e7on 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 appel\u00e9e \"PCP\" ou \"Protected Content Playback\" qui leur permet de bloquer au moins une partie des techniques de r\u00e9cup\u00e9ration du flux vid\u00e9o et audio. Cependant, Firefox de pas sa nature Open Source utilise \"Open H264\" pour lire ces m\u00eames flux soumis \u00e0 des DRM et Open H264 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\u2026 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\" 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 simplement 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\u00e9rer des images de la F1TV : D\u00e9marrer une instance de navigateur avec les bons arguments Ajouter les bons param\u00e8tres 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\u00e8tres Cliquer sur le menu d\u00e9roulant des r\u00e9solutions Trouver l'option 1080P et la s\u00e9lectionner Cliquer sur le bouton qui met la vid\u00e9o en plein \u00e9cran Prendre de screenshots \u00e0 intervalles r\u00e9guliers Pour faire toutes ces actions, on doit r\u00e9cup\u00e9rer 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 (); \u00c7a peut para\u00eetre plut\u00f4t simple dit comme \u00e7a et quand tout fonctionne \u00e7a l'est, mais la difficult\u00e9 vient du fait qu'\u00e0 peu pr\u00e8s n'importe 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. Parfois, il est aussi difficile de trouver un \u00e9l\u00e9ment selon son ID, sa classe, ou sa value. Par exemple, l'option qui permet de passer en 1080P peut avoir comme value 1080_9011456 ou 1080_9011200 si on refresh la page. Cela demande de passer par des expressions r\u00e9guli\u00e8res, ce qui n'est pas compliqu\u00e9 en soi, mais ce sont toutes ces petites choses qui rendent le processus long \u00e0 mettre en place. Il faut dire aussi que les sites ne sont pas forc\u00e9ment tr\u00e8s contents 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\u00e9rentes techniques pour passer outre ces restrictions comme : Changer son User Agent 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 m\u00e9thodes 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 h\u00e9berg\u00e9e autre part que le reste du site chez Amazon et qu'elle poss\u00e8de les meilleures s\u00e9curit\u00e9s que j'aie pu voir. Aucunes des m\u00e9thodes 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\u00e9der au reste du site internet. R\u00e9cup\u00e9rer 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 personne qui utilise mon app. Alors \u00e9videmment \u00e7a n'est pas pour faire des b\u00eatises avec et c'est pour une \"bonne\" raison, mais bon quand m\u00eame \u00e7a peut faire bizarre comme \u00e7a. Je pense que vous savez d\u00e9j\u00e0 ce qu'est un Cookie, mais je vais malgr\u00e9 tout faire un petit point l\u00e0-dessus, car c'est important pour la suite. Quand on va sur un site internet et que l'on se connecte avec nos identifiants, nous sommes connect\u00e9s sur la session. Cependant, si on quitte le site ou que l'on ferme le navigateur, le site ne peut pas garder en m\u00e9moire que c'est bien vous quand le lendemain, vous retournez dessus. Pour palier \u00e0 cette limitation, on a invent\u00e9 cette chose magnifique (hem...) que sont les cookies ! Les cookies sont des petits fichiers qui sont stock\u00e9s dans votre navigateur et qui peuvent servir \u00e0 beaucoup de choses comme traquer votre activit\u00e9 sur internet et espionner un peu ou aussi par exemple, servir de jeton de connexion. L'id\u00e9e est que quand vous vous connectez sur le site avec vos identifiants, le site envoie un petit fichier dans votre navigateur qui va servir de jeton. Et donc lorsque vous reviendrez, le site pourra voir que vous avez le jeton et vous connectera automatiquement. \u00c7a peut para\u00eetre g\u00e9nial, et c'est effectivement bien pratique, cependant ce n'est pas sans risques. En effet, imaginons qu'un acteur malveillant parvienne \u00e0 s'emparer de ces petits fichiers, il pourrait ainsi facilement se faire passer pour vous. Alors un cookie expire \u00e0 un moment donn\u00e9 pour temp\u00e9rer les risques, mais ils sont toujours pr\u00e9sents. Dans notre cas, on peut vite comprendre pourquoi cela peut \u00eatre int\u00e9ressant de r\u00e9cup\u00e9rer ces cookies. En effet, si on peut mettre la main sur le jeton de connexion de l'utilisateur de notre application. On pourra se connecter automatiquement \u00e0 la F1TV et aller prendre des photos directement sans que l'utilisateur ait \u00e0 faire quoi que ce soit. Sauf que les cookies ne sont pas stock\u00e9s en clair comme \u00e7a. \u00c9videmment, Google Chrome a mis en place quelques techniques pour \u00e9viter que n'importe qui puisse s'amuser \u00e0 aller taper dans les cookies de la machine. Tous les cookies sont stock\u00e9s dans une base de donn\u00e9es SQLite avec les noms en clair et les valeurs sont encrypt\u00e9es en utilisant la m\u00e9thode AES 256 qui est une m\u00e9thode de cryptage tr\u00e8s utilis\u00e9e et efficace. Tellement efficace qu'il serait compl\u00e8tement inutile de tenter de les d\u00e9crypter en utilisant de la force brute pour trouver la valeur ou m\u00eame une attaque de dictionnaire ou quoi que ce soit. Si ces valeurs peuvent \u00eatre encod\u00e9es et d\u00e9cod\u00e9es en local sur la machine sans connexion internet, cela veut dire que la cl\u00e9 est stock\u00e9e sur la machine. Et si je peux mettre l\u00e0, mais sur cette cl\u00e9, alors je pourrai lire tous les cookies de la machine. Cette cl\u00e9 est stock\u00e9e dans les fichiers de Google Chrome sous Google\\Chrome\\User Data\\Local State . Et dans ce fichier, on peut trouver une liste de donn\u00e9es en cl\u00e9 valeurs et on peut trouver la cl\u00e9 sous os_crypt encrypted_key . On pourrait croire que l'on a d\u00e9j\u00e0 touch\u00e9 le jackpot, mais il reste encore une \u00e9tape. Cette cl\u00e9 est crypt\u00e9e en utilisant le syst\u00e8me d'encryption de Windows. Cette encryption est utilis\u00e9e pour emp\u00eacher des utilisateurs non connect\u00e9s d'acc\u00e9der \u00e0 certaines donn\u00e9es. Mais comme nous sommes connect\u00e9s, nous pouvons facilement utiliser les librairies de d\u00e9cryptions pour trouver la valeur de cette cl\u00e9. Et \u00e0 partir de l\u00e0, il suffit d'utiliser cette cl\u00e9 pour d\u00e9crypter tous les cookies de la machine pour aller chercher ceux qui nous int\u00e9ressent. Voici un exemple du code python qui permet d'aller chercher la cl\u00e9 d'encryption dans les fichiers de Google Chrome : 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 Python ?? Et oui j'ai choisit d'utiliser un srypt python pour aller chercher les cookies. Ce choix a \u00e9t\u00e9 fait pour trois raisons : Le python est un language que je n'aime pas particuli\u00e8rement mais qui poss\u00e8de un \u00e9ventail de librairies absolument fantastique. Et pour ce genre de choses qui demandent une constante mise \u00e0 jour des librairies et qui sont un peu niches le python est une option juste g\u00e9niale. Comme c'est une des parties qui est le plus suceptible de changer vu que Chrome change relativement souvent le syst\u00e8me de stockage des cookies. Dans une optique de facilit\u00e9 de maintenance, avoir un seul fichier qui concerne cette partie du projet et qui est dans un language que plus de gens maitrisent que C# est pratique. Je n'ai pas r\u00e9ussi \u00e0 trouver de librairies C# qui me donne des r\u00e9sultats identiques \u00e0 celles que j'utilise dans ce script python. Pour faire la liaison entre le C# et le python, j'appelle le script depuis mon C# et ensuite le python s'occupe de mettre tous les cookies dans un CSV qui est ensuite lu depuis le C#. Voici la partie python qui \u00e9crit dans le csv : 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\" ) Et la partie C# qui appelle le script et qui lit le CSV : 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 ; } Maintenant que l'on sait comment simuler et manipuler un navigateur internet, que l'on sait comment se connecter sur le compte F1TV de l'utilisateur sans qu'il n\u2019aie rien \u00e0 faire. On a tous les ingr\u00e9dients pour automatiquement r\u00e9cup\u00e9rer des images de la F1TV du Grand Prix que l'on souhaite. Calibration Maintenant que l'on a des images de la page Data de la F1TV, on pourrait croire que c'est tout bon, on peut direct passer \u00e0 la partie OCR. Mais que nenni ! Le gros souci de l'OCR c'est que sa pr\u00e9cision est grandement r\u00e9duite d\u00e8s que l'on augmente la taille de la zone de recherche. M\u00eame simplement deux mots sur une image, si on les prend dans les images individuelles, on a de grandes chances de trouver quelque chose, mais si on les met les deux sur la m\u00eame et qu'on tente l'OCR, on va avoir de r\u00e9sultats bien moins bons. Et puis il faut aussi voir que selon les donn\u00e9es que je cherche, je ne peux pas faire le m\u00eame traitement. Par exemple, savoir si le DRS est allum\u00e9, savoir quels pneus chausse un pilote et depuis combien de tours et savoir quel est le temps de son dernier tour, ce sont des informations qui demandent des traitements qui n'ont rien \u00e0 voir. Il faut donc pouvoir dire au programme d'OCR ou se trouvent les informations et quelle est leur nature pour qu'il puisse les d\u00e9coder. Il faut donc faire une calibration qui puisse donner toutes les infos importantes, mais qui en m\u00eame temps soit facile \u00e0 utiliser, car un utilisateur doit \u00eatre capable de le faire assez facilement. Voici la liste des informations que l'on doit r\u00e9cup\u00e9rer : La liste des pilotes pr\u00e9sents sur le Grand Prix La position de la zone principale La position de chaque zone de pilote La position de toutes les Windows sur chaque zone de pilote Le but a \u00e9t\u00e9 de retirer le plus d'\u00e9tapes possibles \u00e0 l'utilisateur. Techniquement, j'aurais pu faire une version compl\u00e8tement manuelle, mais \u00e7a aurait pris trop de temps, alors il y a des syst\u00e8mes qui permettent de rendre cette t\u00e2che moins p\u00e9nible. Liste des pilotes Pour la liste des pilotes, j'ai pens\u00e9 \u00e0 utiliser une API externe pour avoir une liste dans laquelle on pourrait s\u00e9lectionner des noms de pilotes, sauf que j'ai abandonn\u00e9 l'id\u00e9e, car je trouvais que le projet avait d\u00e9j\u00e0 bien assez de points qui d\u00e9pendent de l'ext\u00e9rieur. Il y a donc une liste de pilotes dans laquelle on peut ajouter ou supprimer des noms de pilotes. L'id\u00e9al serait de mettre tous les pilotes de r\u00e9serve, comme \u00e7a si un pilote est malade sur une course, on n'a pas besoin de venir changer la liste. Zone principale Pour la zone principale, c'est enti\u00e8rement manuel, on attend de l'utilisateur deux points x, y sur l'image pour ensuite avoir une id\u00e9e d'o\u00f9 est cens\u00e9 se trouver la zone. \"Exemple de zone principale\" Zones pilotes C'est l\u00e0 que \u00e7a devient int\u00e9ressant. L'utilisateur n'a pas besoin de faire quoi que ce soit pour que le programme sache o\u00f9 sont les zones des pilotes. J'aurais pu le faire manuellement en faisant choisir \u00e0 l'utilisateur de donner deux points qui correspondent \u00e0 la premi\u00e8re zone et extrapoler pour en avoir 20. Sauf que si l'utilisateur n'est pas pr\u00e9cis au pixel pr\u00e8s (et m\u00eame comme \u00e7a parfois) le vingti\u00e8me pilote se retrouve avec une zone compl\u00e8tement d\u00e9sax\u00e9e. L\u00e0, le programme va \"simplement\" effectuer une reconnaissance de texte sur toute l'image. Les r\u00e9sultats ne nous int\u00e9ressent pas vraiment, tout ce que l'on veut, c'est la position des textes. Avec les positions, il est facile de d\u00e9terminer o\u00f9 sont toutes les zones de pilotes et donc sans que l'utilisateur ait \u00e0 toucher quoi que ce soit, d\u00e8s qu'il a donn\u00e9 les infos pour la zone principale, les zones de pilotes sont d\u00e9termin\u00e9es. \"Exemple zone pilote\" Voici un exemple du code utilis\u00e9 pour trouver ou dessiner des zones de pilotes : 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 ++; } } Windows pilotes C'est ici que c'est le plus p\u00e9nible pour l'utilisateur, il doit s\u00e9lectionner manuellement les positions des fen\u00eatres de donn\u00e9es. Ensuite, d\u00e8s que l'utilisateur a donn\u00e9 une position pour chaque window, on applique les positions pour chaque zone de pilote. Il y a plusieurs types de windows et selon le type le traitement est diff\u00e9rent comme je l'ai dit plus t\u00f4t. Voici des exemples concrets : \"Exemple Window de pneus\" \"Exemple Window temps au tour\" \"Exemple window Drs\" Il est important que toutes ces zones soient transmises avec le plus de pr\u00e9cision possible pour que l'OCR puisse bien faire son boulot. Stockage Ensuite, quand l'utilisateur a fini de configurer son flux, la configuration est stock\u00e9e pour qu'il puisse ensuite la r\u00e9utiliser pour tous les autres Grand Prix de l'ann\u00e9e. Le stockage est fait sous format JSON et est fait pour que le programme d'OCR puisse lire dedans toutes les infos n\u00e9cessaires. Cela fait des fichiers plut\u00f4t gros, mais je n'avais pas vraiment le choix. J'ai test\u00e9 une version avec seulement les infos de la premi\u00e8re zone de pilote, mais avec l'interpolation, les derniers pilotes se retrouvent avec des zones clairement pas \u00e0 la bonne taille. Voici un exemple de ce \u00e0 quoi ressemble le JSON final : { \"Main\" : { \"x\" : 36 , \"y\" : 343 , \"width\" : 3780 , \"height\" : 1454 , \"DriverZones\" : [ { \"name\" : \"Driver1\" , \"x\" : 0 , \"y\" : 1 , \"width\" : 3780 , \"height\" : 72 , \"Windows\" : [ { \"Position\" : { \"x\" : 45 , \"y\" : 3 , \"width\" : 76 , \"height\" : 65 }, \"GapToLeader\" : { \"x\" : 447 , \"y\" : 1 , \"width\" : 206 , \"height\" : 67 }, \"LapTime\" : { \"x\" : 863 , \"y\" : 3 , \"width\" : 229 , \"height\" : 65 }, \"DRS\" : { \"x\" : 1095 , \"y\" : 1 , \"width\" : 174 , \"height\" : 67 }, \"Tyres\" : { \"x\" : 1274 , \"y\" : 3 , \"width\" : 1448 , \"height\" : 62 }, \"Name\" : { \"x\" : 2724 , \"y\" : 3 , \"width\" : 361 , \"height\" : 65 }, \"Sector1\" : { \"x\" : 3088 , \"y\" : 1 , \"width\" : 239 , \"height\" : 65 }, \"Sector2\" : { \"x\" : 3314 , \"y\" : 4 , \"width\" : 190 , \"height\" : 62 }, \"Sector3\" : { \"x\" : 3493 , \"y\" : 1 , \"width\" : 198 , \"height\" : 67 } } ] }, { \"name\" : \"Driver2\" , \"x\" : 0 , \"y\" : 72 , \"width\" : 3780 , \"height\" : 72 , \"Windows\" : [ { \"Position\" : { \"x\" : 45 , \"y\" : 3 , \"width\" : 76 , \"height\" : 65 }, \"GapToLeader\" : { \"x\" : 447 , \"y\" : 1 , \"width\" : 206 , \"height\" : 67 }, \"LapTime\" : { \"x\" : 863 , \"y\" : 3 , \"width\" : 229 , \"height\" : 65 }, \"DRS\" : { \"x\" : 1095 , \"y\" : 1 , \"width\" : 174 , \"height\" : 67 }, \"Tyres\" : { \"x\" : 1274 , \"y\" : 3 , \"width\" : 1448 , \"height\" : 62 }, \"Name\" : { \"x\" : 2724 , \"y\" : 3 , \"width\" : 361 , \"height\" : 65 }, \"Sector1\" : { \"x\" : 3088 , \"y\" : 1 , \"width\" : 239 , \"height\" : 65 }, \"Sector2\" : { \"x\" : 3314 , \"y\" : 4 , \"width\" : 190 , \"height\" : 62 }, \"Sector3\" : { \"x\" : 3493 , \"y\" : 1 , \"width\" : 198 , \"height\" : 67 } } ] } [ O t her pilo ts ... ] ], \"Drivers\" : [ \"Perez\" , \"Verstappen\" , \"Alonso\" , \"Sainz\" , \"Russel\" , \"Gasly\" , \"Leclerc\" , \"Ocon\" , \"Hulkenberg\" , \"Bottas\" , \"Hamilton\" , \"Albon\" , \"Tsunoda\" , \"Zhou\" , \"Stroll\" , \"De Vries\" , \"Magnussen\" , \"Norris\" , \"Piastri\" , \"Sargeant\" ] } } Et avec tout \u00e7a. L'OCR peut d\u00e9marrer dans de bonnes conditions OCR Maintenant qu'on a des images qui arrivent automatiquement et que l'on sait o\u00f9 se trouvent les informations sur ces dites images, 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 selon 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 \u00e7a, 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 quatre zones contenant de l'information dans un format diff\u00e9rent. \"Zones principales\" Dans l'exemple ci-dessus, on peut voir trois zones, mais on aurait \u00e9galement pu comprendre la zone de position des pilotes autour du circuit pour faire 4. Ces quatre 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 \u00e7a \u00e0 impl\u00e9menter. J'ai utilis\u00e9 le mot \"Zone\" plus haut et \u00e7a 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 fen\u00eatres ainsi qu'une m\u00e9thode 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 m\u00e9thode qui permet de renvoyer ce qui peut \u00eatre d\u00e9cod\u00e9 sur son image. Les enfants peuvent aussi aller piocher dans les nombreuses m\u00e9thodes de r\u00e9cup\u00e9ration de donn\u00e9es contenues dans le parent Window. Il vaut mieux 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 biblioth\u00e8que. Si c'est une zone qui contient d'autres zones, c'est une biblioth\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 contiennent de l'information et sont stock\u00e9s dans des armoires et on y acc\u00e8de en allant dans la bonne biblioth\u00e8que et en allant dans la bonne armoire. Derni\u00e8res choses pour comprendre le diagramme : Il existe une Main Zone qui est une des quatre 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. Voil\u00e0 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 simplement les d\u00e9coder. Maintenant qu'on a une liste de diff\u00e9rents types de zones, on peut commencer \u00e0 chercher ce qu'il y a marqu\u00e9 dessus. Pour cela, il faut d'abord comprendre un petit peu comment l'OCR fonctionne et comment des libraires 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 qu'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 d\u00e9tecter les points communs entre les lettres pour cr\u00e9er un alpphabet. Par exemple, la matrice de la lettre 'H' donnerait un poids important \u00e0 des lignes verticales connect\u00e9es par une ligne centrale. Et si on fournit assez de donn\u00e9es de bonne qualit\u00e9 au mod\u00e8le, les matrices peuvent \u00eatre tr\u00e8s efficace \u00e0 d\u00e9tecter si une lettre est un H ou un M. Il y a pleins d'autres m\u00e9thodes 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 \u00e7a 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 inconsistants. Dans notre cas, le souci est que les chiffres et lettres sont beaucoup trop petits. Ils ne font parfois que 10 pixels de haut et cela fait qu'il n'est pas forc\u00e9ment ais\u00e9 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 qu'\u00e0 peu pr\u00e8s n'importe 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 antialiasing\" On voit que le 9 n'est pas clairement d\u00e9fini. En effet, on pourrait le comprendre comme : \"Premier exemple de contours\" Ou comme : \"Second exemple de contours\" Voire simplement comme : \"Exemple de contour g\u00e9n\u00e9reux\" Et on se rend bien compte que les performances de d\u00e9tection 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 m\u00e9thodes 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 \u00e0 plus de mal \u00e0 reconna\u00eetre, 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 puisque si une lettre pose un probl\u00e8me, 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 qu'il \u00e9tait souvent plus facile de trouver un noir sur blanc que blanc sur noir. Je ne suis pas s\u00fbr que cette \u00e9tape soit capitale cependant \"Texte invers\u00e9\" 2 : Je fais un Treshhold de 165, car avec moins le texte occasionnellement 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 m\u00e9thode 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 m\u00e9thodes pr\u00e9cises plus bas Voil\u00e0 pour ce qui est du post processing. Je ne dis pas que ce sont les meilleurs param\u00e8tres possibles, mais dans mes tests ce sont ceux qui ont le mieux march\u00e9s. Ce sont aussi les premi\u00e8res m\u00e9thodes 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 qu'on sait d\u00e9j\u00e0 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 qu'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 m\u00e9thode appel\u00e9e la distance de Levenshtein. Pour faire simple, c'est une m\u00e9thode qui va calculer les distances de lettres pour d\u00e9terminer entre des strings laquelle ressemble le plus \u00e0 une autre. Pour r\u00e9sumer le fonctionnement dans l'ordre : 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 m\u00e9thode en r\u00e9alit\u00e9 utilise juste la m\u00eame m\u00e9thode que celle qui va r\u00e9cup\u00e9rer le texte sur une image. Cependant, l\u00e0, 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 \u00e7a me soit arriv\u00e9 tr\u00e8s r\u00e9guli\u00e8rement et que \u00e7a me soit rest\u00e9 dans la gorge \u00e9videmment). L'avantage, c'est que cette m\u00e9thode ne demande m\u00eame pas de traitement de la donn\u00e9e en sortie de Tesseract. On esp\u00e8re simplement que le post traitement aura suffit. TEMPS : Cette m\u00e9thode 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 deux 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 \u00e7a. 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 \u00e9chelles 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\u00e8tement impr\u00e9visible. \u00c7a n'est simplement pas utilisable. Cette partie est un peu plus complexe, car si la d\u00e9tection n'est pas fiable, les chiffres sont juste 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 ambigu\u00eft\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 ressemblent beaucoup plus que les lettres, il faut tenter le plus possible de conserver leurs 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 m\u00e9thode 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 \u00c9rosion qui me permet de contrecarrer en partie les rondeurs ajout\u00e9es par la dilatation et retrouver des chiffres bien form\u00e9s. Pour l' \u00c9rosion 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 \u00c9rosion\" Explication des m\u00e9thodes pr\u00e9cises plus bas Et avec ce post processing, on retrouve de plut\u00f4t bons 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 qu'il va falloir transformer en millisecondes 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 d\u00e9tecter quand \u00e7a arriver. Je passe les d\u00e9tails du reste du nettoyage, car c'est vraiment du cas par cas, mais quand on a fini de nettoyer la chaine, on peut transformer les chaines de minutes, secondes et millisecondes en un total de millisecondes. 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 convertit le r\u00e9sultat en millisecondes les chiffres (2) Il faut savoir qu'avec la derni\u00e8re version de l'\u00e9mulateur (dont je vais parler un peu plus tard). Pneus L\u00e0, 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 pneu 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 \u00e7a : \"Exemple zone pneus 2\" Mais aussi \u00e0 \u00e7a : \"Exemple zone pneus 3\" Voire m\u00eame \u00e7a : \"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\u00e9ration 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 tours que le pilote a pass\u00e9 sur ce pneu. La couleur indique le type de pneu. S'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 m\u00e9thode 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 d\u00e9terminer 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\u00e8te\" Elle est automatiquement coup\u00e9e de cette fa\u00e7on : \"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 d\u00e9tection 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 m\u00e9thode qui calcule la diff\u00e9rence entre la couleur obtenue 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 o\u00f9 il n'y a pas beaucoup de couleur comme celui-l\u00e0 : \"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 d\u00e9terminer 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 n'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 sympathique de la lecture du chiffre. Vous saurez que Tesseract, en plus de d\u00e9tester les grandes images et les images avec des couleurs, d\u00e9teste \u00e9galement les formes dans une image. Ainsi dans notre cas, le round de couleur autour du chiffre, m\u00eame s'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 alors retirer le background AUTOUR du rond, et ensuite si on retire la couleur, il devrait rester le chiffre sur fond blanc. Pour ce 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 peut retirer les pixels qui ont une valeur dans un channel RGB plus haute qu'un certain seuil : \"Zone avec le reste des couleurs supprim\u00e9es\" Et l\u00e0, on a ce que l'on veut ! \u00c0 partir de l\u00e0, ce sont 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 m\u00e9thodes 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 \u00e7a, 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 m\u00e9thodes sur les images Dans ce projet, on a d\u00fb utiliser diff\u00e9rentes m\u00e9thodes d'\u00e9dition d'image, que ce soit sous forme de filtres ou de modification de l'image directement. Voici un sommaire des m\u00e9thodes utilis\u00e9es et comment elles fonctionnent. Tresholding Cette m\u00e9thode 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'algorithme fonctionne sachant qu'il demande en entr\u00e9e la Bitmap que l'on veut modifier ainsi que la valeur de Treshold : On parcourt chaque pixel de l'image On convertit la couleur du pixel en une valeur de gris pour avoir la m\u00eame valeur en R,G et B (Formule utilis\u00e9e : gray = 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 algorithme pas forc\u00e9ment complexe, mais qui peut augmenter de mani\u00e8re titanesque les chances de r\u00e9ussir une OCR Resize Cette m\u00e9thode sert \u00e0 augmenter la r\u00e9solution d'une image pour am\u00e9liorer la pr\u00e9cision de l'algorithme 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 m\u00e9thode d'augmentation de la taille avec une simple interpolation. En effet, une augmentation de taille interpol\u00e9e ne va pas vraiment changer la r\u00e9solution, l'image sera toujours aussi pixelis\u00e9e, seulement, les pixels seront compos\u00e9s 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 d\u00e9tails, 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\u00e9j\u00e0 pr\u00e9sentes 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 d\u00e9tails. 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 \u00c9rosion Cette m\u00e9thode et la suivante font partie des m\u00e9thodes de transformation morphologiques. Ces m\u00e9thodes sont utilis\u00e9es pour accentuer les formes et les \u00e9paissir ou les r\u00e9duire et les affiner. Elles poss\u00e8dent l'aventage aussi de retirer le flou d'une image ce qui est tr\u00e8s pratique si utilis\u00e9 apr\u00e8s l'utilisation de m\u00e9thodes comme Resize . Je ne vais pas trop rentrer dans les d\u00e9tails de ces m\u00e9thodes, car leur fonctionnement est un peu plus lourd en math si on veut faire une v\u00e9ritable explication du pourquoi et du comment \u00e7a 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 m\u00e9thode est assez simple et est juste une m\u00e9thode 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 ambigu\u00ebs. 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 sophistiqu\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 d\u00e9tails, voir la d\u00e9tection de pneus. Il y a aussi d'autres m\u00e9thodes comme un filtre Gaussien ou Highlight countour que j'ai d\u00fb d\u00e9velopper, mais que je n'ai pas utilis\u00e9 donc je ne vais pas en parler ici. Petit point r\u00e9solution Comme on peut l'imaginer, la r\u00e9solution est extr\u00eamement importante pour l'OCR. Et en avan\u00e7ant sur le projet de l'\u00e9mulateur, je me suis rendu compte qu'il \u00e9tait possible de r\u00e9cup\u00e9rer des images en 4K (Plut\u00f4t 1080 avec l'upscaling du lecteur). Cela est une superbe nouvelle car cela permet de simplifier \u00e9norm\u00e9ment le processing sur les diff\u00e9rentes windows. Quelques exemples pour se faire une id\u00e9e \"Echantillon 720P\" ; \"Echantillon 1080P\" \"Echantillon 4K\" Mais il faut savoir que gr\u00e2ce \u00e0 cette simplification, j'ai pu aussi cr\u00e9er d'autres m\u00e9thodes de filtrage pour certaines parties. Mais la simplification \u00e9tait obligatoire, car avec des images aussi grandes, il n'\u00e9tait simplement pas possible de venir appliquer les m\u00eames filtres car le temps de traitement serait beaucoup plus long. J'indique ces changements que apr\u00e8s l'explication d'avant car ce sont des changements un peu de derni\u00e8re minute et que la logique expliqu\u00e9e plus haut a \u00e9t\u00e9 tr\u00e8s importante pour le projet OCR m\u00eame si tout n'est plus forc\u00e9ment utilis\u00e9 maintenant que j'ai des images de meilleure qualit\u00e9. Dans la version actuellement disponible, la reconnaissance a \u00e9t\u00e9 simplifi\u00e9e sous cette forme : Le \"GapToLeader\" est d\u00e9cod\u00e9 avec un premier passage de Tresholding \u00e0 165 puis un Resize de 2 et une Dilatation de 1 pour retirer le flou Les \"Sectors\" sont d\u00e9cod\u00e9s en utilisant une toute nouvelle m\u00e9thode VanishOxyAction \u00e0 cause des couleurs parfois appliqu\u00e9es et ensuite simplement une methode de Tresholding de 150 pour rendre le r\u00e9sultat assez propre pour l'OCR. Le \"LapTime\" est d'abord pass\u00e9 par un Tresholding tr\u00e8s strict de 185 pour pr\u00e9parer la SobelEdgeDetection qui est \u00e9galement une nouvelle m\u00e9thode qu'il a \u00e9t\u00e9 possible d'utiliser gr\u00e2ce \u00e0 la simplification du reste des processus. Le \"Text\" est d\u00e9cod\u00e9 juste avec un tresholding de 165 d\u00e9sormais gr\u00e2ce \u00e0 l'image 4K. Les pneus ont leur propre traitement comme expliqu\u00e9 plus haut auquel on ajoute la Dilatation de 1. Comme on peut le voir, le traitement est pas mal plus simple, mais cela ne veut pas dire que les autres m\u00e9thodes que je n'utilise plus ne sont pas utiles. La reconnaissance n'est pas encore parfaite et je pense que leur utilisation pourrait aider \u00e0 am\u00e9liorer les r\u00e9sultats. (Et parfois ces anciennes m\u00e9thodes sont utiles dans les traitements personnalis\u00e9s des Windows elle m\u00eame comme par exemple les pneus qui utilisent la m\u00e9thode GrayScale pour isoler les couleurs) VanishOxyAction Cette m\u00e9thode est une m\u00e9thode plut\u00f4t simple, mais qui est importante. Elle se base beaucoup sur le code de la m\u00e9thode Grayscale et sur la m\u00e9thode Tresholding car elle essaie de regrouper le meilleur d\u00e8s deux en r\u00e9glant quelques soucis que ces derni\u00e8res cr\u00e9aient. Les soucis avec la m\u00e9thode grayscale c'est que quand le texte est de couleur (Ce qui arrive souvent pour les temps de secteurs) la m\u00e9thode GrayScale rend les couleurs dans une nuance de gris un peu trop sombre ce qui fait qu'ensuite la m\u00e9thode de Tresholding d\u00e9fonce tout. \"Exemple de secteur en couleur\" \"Exemple de secteur en grayscale\" L'id\u00e9e est alors de prendre pour chaque pixel et de garder uniquement la valeur de R, G ou B la plus haute et de mettre les deux autres canaux au m\u00eame niveau pour avoir une image blanchie qui puisse \u00eatre ensuite utilis\u00e9e avec la m\u00e9thode de Tresholding sans soucis. \"Exemple de secteur blanchi avec vanishoxyAction\" SobelEdgeDetection On pourrait se dire qu'avec ce genre de m\u00e9thode le tresholding est inutile ensuite, mais \u00e7a n'est pas le cas, car le tresholding sert ensuite pour rendre les contours plus ou moins agressif. Parce que m\u00eame si l'image ressemble \u00e0 une image binaris\u00e9e, il reste des nuances que le treshold va pouvoir utiliser. SobelEdgeDetection Cette m\u00e9thode est une m\u00e9thode assez classique que je n'ai pas design\u00e9 moi-m\u00eame alors, je ne vais pas trop m'\u00e9pancher dessus. En gros, on utilise une matrice et une formule math\u00e9matique pour redessiner une image et le r\u00e9sultat est une image avec des contours. Je ne l'ai utilis\u00e9 que pour les temps au tour, car ce sont les plus r\u00e9calcitrants. Cette m\u00e9thode a besoin d'une image pass\u00e9e en noir et blanc au pr\u00e9alable \u00e0 laquelle on applique ensuite les matrices de filtres. Et avec ces filtres ajout\u00e9s \u00e0 l'image, on peut ensuite calculer le \"Gradient\" pour cr\u00e9er les bords. Le seul souci de cette m\u00e9thode, c'est qu'elle est assez gourmande et qu'elle fournit des formes creuses d\u00fb \u00e0 la nature des matrices donn\u00e9es. Voici un exemple de ce dont cette m\u00e9thode est capable : Artefacts de la d\u00e9tection de bords de Sobel Apparemment l'OCR aime assez bien cette m\u00e9thode et elle permet de beaucoup moins souvent oublier les '.' ou ':' Traitement des donn\u00e9es C'est bien gentil de recevoir des r\u00e9sultats de l'OCR, cependant on ne peut pas souvent les utiliser comme tels. En effet, les r\u00e9sultats ne sont pas tr\u00e8s constants et demandent d'\u00eatre v\u00e9rifi\u00e9s pour savoir s'ils doivent \u00eatre pris en compte. Le post traitement de ces donn\u00e9es d\u00e9pend compl\u00e8tement du contexte et donc il est diff\u00e9rent pour chaque type de window. Voici un floril\u00e8ge des diff\u00e9rents types de traitements : Traitement du nom de pilote Rien de plus que ce qui est d\u00e9j\u00e0 d\u00e9taill\u00e9 dans la partie OCR Traitement des pneus Pareil Traitement des temps L\u00e0, par contre, c'est int\u00e9ressant. Dans un monde parfait, je pourrais simplement prendre les r\u00e9sultats de l'OCR et les traiter directement. Mais comme nous vivons dans un monde ou la souffrance et la douleur sont les seules choses autoris\u00e9es, on ne peut pas. Le probl\u00e8me vient du fait que les temps que l'on peut trouver sur la F1TV sont encod\u00e9s avec des '.' et des ':' qui d\u00e9terminent les limites entre les chiffres qui d\u00e9signent les minutes, les secondes et les millisecondes. Et le souci avec ces s\u00e9parateurs, c'est qu'ils aiment bien mettre le chaos dans la reconnaissance. Quand ils ne sont pas compris comme des autres chiffres, ils sont parfois juste oubli\u00e9s ou pris en double, c'est un enfer. Il faut donc trouver un moyen de d\u00e9tecter quand cela arrive. Et je n'ai pas trouv\u00e9 de meilleurs moyens que de faire du cas par cas. Cela peut para\u00eetre simple quand on parle par exemple des secteurs. On sait qu'on attend deux chiffres avant un '.' et trois chiffres apr\u00e8s. Il est ainsi facile de voir que si je trouve six chiffres et pas de s\u00e9paration, le troisi\u00e8me est le s\u00e9parateur mal compris. Mais l'exemple qui d\u00e9truit vraiment tout, ce sont les \u00e9carts avec le leader. Autant un temps au tour, c'est toujours x:xx.xxx et un temps de secteur, c'est xx.xxx. Mais un \u00e9cart avec le leader \u00e7a peut \u00eatre 0.345 comme 1:12.345. Ce qui fait que lorsque je re\u00e7ois 121345 est-ce que c'est 12.345 ou 1:21.345...? Souvent, on peut quand m\u00eame d\u00e9duire, mais cela demande de pr\u00e9voir presque tous les cas limites, ce qui est assez p\u00e9nible. On pourrait se dire qu'il suffit de voir si la valeur est trop en dehors des normes. Le souci, c'est qu'il n'est pas impossible qu'un temps au tour ou un \u00e9cart prenne d'un coup une grosse diff\u00e9rence. Cela arrive m\u00eame assez souvent quand des pilotes sortent de la piste. \"Exemple temps au tour\" \"Exemple temps secteur\" Pour ce qui est du DRS et de la position des pilotes, il n'y a pas vraiment de traitement suppl\u00e9mentaire. Non pas, car la d\u00e9tection est parfaite, mais par ce que la d\u00e9tection ne peut pas rater de 200 fa\u00e7ons. Le DRS ne peut retourner que TRUE ou FALSE et la position du pilote est entre 1 et 20 compris. Le peu de nuance fait que ce sont des cas de figures qui ne demandent pas un traitement particulier au-del\u00e0 de l'OCR Stockage des donn\u00e9es Dans ce projet, le but n'est pas simplement de trouver les donn\u00e9es et les afficher. L'int\u00e9r\u00eat de les r\u00e9cup\u00e9rer est de pouvoir les comparer \u00e0 d'autres donn\u00e9es pr\u00e9c\u00e9dentes. Le vrai souci de la F1TV c'est justement que l'on ne peut pas facilement voir les \u00e9volutions. On ne peut voir que des \"photos\" de la situation actuelle de la course. Il faut donc garder en m\u00e9moire les diff\u00e9rentes choses qui se sont pass\u00e9es. Techniquement, on pourrait stocker ces donn\u00e9es dans de b\u00eates listes C#. Mais le souci avec \u00e7a, c'est que m\u00eame si des outils comme LinQ existent, \u00e7a n'est pas le plus pratique quand on veut faire des recherches complexes. Il faut aussi voir que si le projet dans sa forme actuelle aurait peut-\u00eatre pu se satisfaire de listes simples, le but est d'ensuite pouvoir construire sur ces bases pour faire des pr\u00e9dictions et des insertions de stats beaucoup plus int\u00e9ressantes qui demandent de faire des requ\u00eates complexe rapidement. Je me suis dit que la meilleure m\u00e9thode serait d'avoir une base de donn\u00e9e dans laquelle je peux faire des requ\u00eates SQL. Mais, comme je n'ai pas besoin de toutes les features de SQl et que je ne veux pas avoir \u00e0 g\u00e9rer un serveur de base de donn\u00e9e et tout ce qui va avec, je me suis dit qu'une bonne option serait d'utiliser SQLite. \"Logo SQLite\" ; SQLite est vraiment pratique, car cela me permet d'avoir une DB sans avoir de serveur, donc pas vraiment complexe ou quoi que ce soit, mais qui conserve les avantages de rapidit\u00e9 et d'utilisation de requ\u00eates SQL. J'ai cr\u00e9\u00e9 trois tables dans cette base de donn\u00e9e SQLite que voici : Base de donn\u00e9e Drivers Colonne Type de Data Description Tag ID INTEGER ID du pilote PRIMARY Name VARCHAR Nom du pilote NOT NULL Pitstops Colonne Type de Data Description Tag Lap INTEGER Tour durant lequel le Pitstop a \u00e9t\u00e9 effectu\u00e9 PRIMARY DriverID INTEGER Pilote qui a effectu\u00e9 le Pitstop PRIMARY Tyre VARCHAR Pneu chauss\u00e9 par le pilote NOT NULL Stats Colonne Type de Data Description Tag Lap INTEGER Tour durant lequel le Pitstop a \u00e9t\u00e9 effectu\u00e9 PRIMARY DriverID INTEGER Pilote qui concern\u00e9 PRIMARY Tyre VARCHAR Pneu chauss\u00e9 par le pilote NOT NULL LapTime INTEGER Temps au tour (MS) NOT NULL Sector1 INTEGER Temps du secteur 1 (MS) NOT NULL Sector2 INTEGER Temps du secteur 2 (MS) NOT NULL Sector3 INTEGER Temps du secteur 3 (MS) NOT NULL GapToLeader INTEGER Ecart avec le leader (MS) NOT NULL Position INTEGER Position pilote NOT NULL La table Drivers sert juste \u00e0 stocker les diff\u00e9rents noms de pilote pour qu'ils soient utilis\u00e9s dans le reste de la DB La table Pitstops n'est pas vraiment utilis\u00e9e dans l'\u00e9tat actuel du projet. Mais le but \u00e9tait de la remplir d\u00e8s que le programme d\u00e9tectait un arr\u00eat aux stands. Le but est ensuite de pouvoir construire un classement pond\u00e9r\u00e9 en fonction des arr\u00eats des diff\u00e9rents pilotes et d'afficher la stats tout le temps sur l'affichage principal. Elle n'est pas r\u00e9ellement utilis\u00e9e, car la d\u00e9tection de pitstop n'a pas pu \u00eatre compl\u00e9t\u00e9e. De par la nature des donn\u00e9es r\u00e9cup\u00e9r\u00e9es des pneus et des positions, c'est tr\u00e8s difficile de d\u00e9tecter avec pr\u00e9cision un arr\u00eat aux stands. La table Stats est la plus importante parce qu'elle contient toutes les informations concernant les pilotes \u00e0 chaque tour. L'id\u00e9e est qu'elle soit remplie \u00e0 chaque tour. Les infos ne sont pas cens\u00e9es \u00eatre les infos lives, mais plut\u00f4t juste une photo \u00e0 chaque tour de la situation de chaque pilote pour ensuite pouvoir faire des comparaisons tours par tours. Des donn\u00e9es comme le GapToLeader peuvent \u00e9voluer pendant le tour, mais on s'en fiche. Ce qui compte vraiment, c'est le temps au tour et les secteurs ainsi que les pneus. Quand remplir la base ? Dans ce projet, il y a deux types d'information. Les informations lives qui sont stock\u00e9s dans des listes et les informations long terme qui sont stock\u00e9es dans la DB. \u00c0 chaque it\u00e9ration de l'OCR, les donn\u00e9es r\u00e9cup\u00e9r\u00e9es sont stock\u00e9es dans une liste de DRIVERDATA. Les DRIVERDATA sont des structures de donn\u00e9es qui contiennent toutes les infos d'un pilote \u00e0 un instant T. Elles peuvent \u00eatre incompl\u00e8tes et sont juste l\u00e0 pour faire de petits calculs et d\u00e9terminer quand ins\u00e9rer des donn\u00e9es permanentes. Ce qui nous am\u00e8ne au moment int\u00e9ressant. Comment on d\u00e9termine quand il est int\u00e9ressant d'ins\u00e9rer des informations dans la base de donn\u00e9es. Il y a deux cas de figure ou on pourrait vouloir ins\u00e9rer des infos : Quand un pilote a fini un tour En effet, j'ai estim\u00e9 que les seuls moments o\u00f9 on veut garder une photo de la situation du pilote, c'est, car il passe d'un tour \u00e0 l'autre. Le raisonnement est le suivant : On ne veut pas conserver TOUTES les donn\u00e9es parce que si on prend une photo toutes les trois secondes, la majorit\u00e9 des informations seront redondantes avec les pr\u00e9c\u00e9dentes. Mais en m\u00eame temps, il ne faut pas rater des changements importants de donn\u00e9es. Les seules donn\u00e9es qui changent entre deux passages de l'OCR sont les \u00e9carts entre les pilotes et de temps en temps un nouveau secteur s'affiche. Alors que d'un tour \u00e0 l'autre presque toutes les informations changent. Et on ne perd que les l\u00e9g\u00e8res fluctuations des \u00e9carts entre les pilotes. J'ai donc d\u00e9cid\u00e9 de conserver une photo par tour. Mais c'est bien joli sauf qu'il reste une difficult\u00e9 : Comment savoir qu'un pilote a fait son tour ? Cela peut para\u00eetre simple comme question, mais elle est plus difficile qu'il n'y parait. Il faut savoir qu'en F1 un pilote peut \u00eatre dans son 26\u1d49 tour pendant qu'un autre en est \u00e0 son 24\u1d49. Chaque pilote a sa propre course et au fur et \u00e0 mesure que les \u00e9carts se creusent, il peut y avoir un tour voir plusieurs d'\u00e9cart entre la queue de course et les premiers pilotes. Ensuite, il faut savoir qu'il n'est pas marqu\u00e9 sur la f1TV dans quel tour chaque pilote est. Il faut donc le d\u00e9duire en fonction des Data. Voici le code le if qui d\u00e9tecte un nouveau tour if ( DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 1 ]. Sector3 != 0 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. Sector3 == 0 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. Position != - 1 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 1 ]. Position != - 1 ) { //Do stuff } DriverDataLogs est une liste de liste d'informations de pilotes. Chaque DriverDataLogs repr\u00e9sente les 20 photos des donn\u00e9es des pilotes. Cela veut dire que DriverDataLogs[3] repr\u00e9sente toutes les infos des pilotes dans le tour 4 et que DriverDataLogs[3][0] repr\u00e9sente toutes les infos du premier pilote dans le tour 3. Si on analyse un peu ce qui est \u00e9crit avec ces informations, on peut voir que je d\u00e9termine qu'un nouveau tour se d\u00e9finis comme une photo ou le troisi\u00e8me secteur a \u00e9t\u00e9 compl\u00eat\u00e9 et ou il ne l'\u00e9tait pas juste avant. Cela fait sens car quand un pilote compl\u00eate son troisi\u00e8me secteur c'est la que son dernier temps au tour se met \u00e0 jour. Le reste des tests est juste la pour \u00e9viter les faux positifs dans le cas ou un des deux DriverDataLogs[x][x] soit corrompu et que donc la valeur n'aie pas de sens. Cela veut dire que dans des conditions tr\u00e8s sp\u00e9cifiques je pourrais potentiellement rater un tour mais il faudrait vraiment que l'OCR me joue un vilain tour. Quand un pilote a fait un arr\u00eat aux stands Et la on touche le plus difficile. Pourtant un arr\u00eat aau stand ne devrait pas \u00eatre compliqu\u00e9 \u00e0 detecter. C'est quand un pilote change de pneu. Alors il peut changer de pneu en gardant le m\u00eame type de pneu et donc tout repose sur le nombre de tour qu'un pneu fait. Sauf que il faut ajouter \u00e0 cette reflexion qu'un pneu peut \u00eatre chauss\u00e9 sans qu'il soit neuf. Ce qui veut dire que l'on ne peut pas simplement choisir qu'un pilote a chang\u00e9 de pneus quand ses pneus sont \u00e0 1 tour. Il peut tr\u00e8s bien reprendre des pneus de qualif qui peuvent avoir 10 tours dans les pattes. En plus quand le pilote change de pneus il y a un phenomene assez p\u00e9nible qui fait que les deux premiers tours faits avec ne sont pass vraiment d\u00e9chiffrables car ils sont un peu cach\u00e9s derri\u00e8re la lettre qui indique le nouveau pneu chauss\u00e9. Exemple : \"Infographie de pneu qui se chevauche\" Voici le code que j'avais \u00e9crit pour tenter de trouver quand un pilote avait fait un pitstop : if ( data . CurrentTyre . Coumpound != Tyre . Type . Undefined && data . CurrentTyre . NumberOfLaps == 0 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. CurrentTyre . NumberOfLaps != 0 ) { //Do stuff } On peut voir que j'essaie de detecter quand le pneu est \u00e0 0 tours (ce qui est le moment ou il y a une lettre \u00e0 la place d'un num\u00e9ro de tour) et que la photo d'avant montrait un pneu normal. On v\u00e9rifie aussi que le pneu a bien \u00e9t\u00e9 detect\u00e9 en verifiant que le pneu n'est pas de type undefined. Le soucis c'est que ce n'est pas rare que l'OCR nous retourne qu'un pneu est vieux de 0 tours et donc il est absolument impossible de faire confiance \u00e0 cette metric. Si on veut utiliser cette methode pour trouver les Pitstop il va falloir avant tout am\u00e9liorer l'OCR sur ce point. Ce soucis mets en lumi\u00e8re un principe assez important de l'informatique \"Ggarbage in, Garbage out\". Si les donn\u00e9es que je recoit ne sont pas g\u00e9niales, le r\u00e9sultat ne sera pas g\u00e9nial non plus. Ce qui est frustran,t c'est que la detection des pneus n'est pas si mal mais entre les chiffres qui se chevauchent ce qui nous fait lire 0 alors que c'est juste un un '1' derri\u00e8re un 'H' ou le 1% du temps ou le programme se trompe, on ne peut pour l'instant tout simplement rien faire de mieux. Affichage des donn\u00e9es Maintenant que l'on a stock\u00e9 toutes ces donn\u00e9es, il faut en faire quelque chose sinon ca ne sert a rien. Afficher les donn\u00e9es est techniquement la partie la plus simple du projet. Il faut prendre les donn\u00e9es qui nous int\u00e9ressent de la base de donn\u00e9es et des r\u00e9sultats de l'OCR et les afficher sur des composants Windows Forms. Il y a deux types d'affichages actuellements mis en place dans le projet sur les trois pr\u00e9vus initialement : Affichage direct L'affichage direct est simplement l'affichage du r\u00e9sultat de l'OCR. Par exemple le classement live ainsi que les \u00e9carts entre les pilotes sont affich\u00e9s directement depuis les r\u00e9sultats de l'OCR. Ce ne sont pas forc\u00e9ment des donn\u00e9es prises dans la base de donn\u00e9e. Voici un exemple : for ( int driverCount = 0 ; driverCount < liveData . Count ; driverCount ++) { DriverData driver = liveData [ driverCount ]; lblDriverName . Text = driver . Name ; lblDriverPosition . Text = driver . Position ; lblDriverLapTime . Text = Reader . ConvertMsToTime ( driver . LapTime ); } C'est l'affichage le plus simple et celui qui est le plus soumis \u00e0 des erreurs. En effet, si un pilote est mal detect\u00e9 on le verra directement mal s'afficher dans la Form principale. C'est \u00e9galement l'affichage le moins int\u00e9ressant car il ne cr\u00e9e aucune information, il ne fait que remontrer les infos que l'on peut d\u00e9ja voir dans la F1TV \"Exemple d'affichage live\" Affichage calcul\u00e9 La encore il y a plusieurs types d'affichages : Affichage Hybride Pour ce qui est des affichages hybrides ce sont des affichages qui vont chercher des informations lives et des informations dans la base de donn\u00e9e. Ils ne font pas de calculs \u00e0 proprement parler mais ils affichent plus d'informations que ce que montre la F1TV. Cela veut dire qu'ils repr\u00e9sentent un d\u00e9but de plusvalue par rapport \u00e0 l'alternative qu'est la page DATA de la F1TV. Ils ne sont pas beaucoup plus durs \u00e0 impl\u00e9menter mais demandent de faire des requ\u00eates \u00e0 la base de donn\u00e9e. Comme exemple d'affichage hybride on a l'affichage des cinq derniers tours d'un pilote. C'est un affichage qui ne demande aucun calcul mais qui permet deja de se rendre compte de si le pilote est enn train de perdre ou gagner du temps ou si il est constant, ce qui n'est pas possible actuellement avec la F1TV \u00e0 moins d'avoir une excellente m\u00e9moire. \"Fen\u00eatre d'informations \u00e0 propos d'un pilote\" Voici un exemmple du type de code necessaire pour afficher ce genre de donn\u00e9es: public List <( int LapTime , int Lap )> GetDriverLaptimes ( string driverName , int numberOfLaptimes ) { int driverId = GetDriverID ( driverName ); List <( int LapTime , int Lap )> lapData = new List <( int LapTime , int Lap )>(); string selectQuery = \"Select LapTime,Lap from Stats WHERE DriverID = @driverID ORDER BY Lap DESC LIMIT @limit\" ; using ( var command = new SQLiteCommand ( selectQuery , Connection )) { command . Parameters . AddWithValue ( \"@driverID\" , driverId ); command . Parameters . AddWithValue ( \"@limit\" , numberOfLaptimes ); SQLiteDataReader reader = command . ExecuteReader (); while ( reader . Read ()) { int lapTime = reader . GetInt32 ( 0 ); int lap = reader . GetInt32 ( 1 ); lapData . Add (( lapTime , lap )); } } return lapData ; } public void Display (){ List <( int LapTime , int Lap )> lapsInfos = Storage . GetDriverLaptimes ( driverName , 5 ); int id = 0 ; foreach (( int LapTime , int Lap ) lapData in lapsInfos ){ Labels [ id ]. Text = LapTime ; id ++; } } Note: Le code montr\u00e9 ici n'est pas forc\u00e9ment le code utilis\u00e9 dans le projet. D'une certaine facon les fen\u00eatres de bataille et de d\u00e9passements sont aussi des hybrides. \"Exemple fen\u00eatre des batailles\" Ici ce sont les batailles qui sont repr\u00e9sent\u00e9es. Aucune donn\u00e9e n'est calcul\u00e9e, c'est litterallement directement les donn\u00e9es de la F1TV, mais la nuance est qu'on ne montre que les pilotes qui sont en train de se battre et que on leur assigne une couleur selon \u00e0 quel point ils sont proches. On a une plus-value sur la F1TV sans pour autant faire de monstres calculs. Pour info, les pilotes consid\u00e8r\u00e9s comme \u00eatant en train de se battre sont les pilotes \u00e0 moins de trois secondes les uns des autres et les couleurs sont suivantes : Vert : Dans la zone de DRS (Moins d'une seconde) Jaune : Plus d'une seconde Noir : Plus de deux secondes \"Exemple fen\u00eatre des d\u00e9passements\" La c'est l'historique des d\u00e9passements qui est affich\u00e9. On pourrait presque dire que c'est un affichage compl\u00eatement calcul\u00e9 car ce ne sont pas des informations disponibles directement sur la F1TV cependant je dirais que cela reste un hybride car il n'y a aucuns calculs. On regarde juste les diff\u00e9rences entre l'ancienne position d'un pilote et la nouvelle et on affiche les changements. Affichage totalement calcul\u00e9 L'affichage compl\u00eatement calcul\u00e9 est un type d'affichage qui ne montre aucune information trouv\u00e9e sur la page de la F1TV. C'est le premier affichage \u00e0 traiter l'information qu'il trouve et il retourne des informations nouvelles. La nuance avec les affichages pr\u00e9dictif est qu'il ne cr\u00e9e pas r\u00e9ellement de l'information, il la d\u00e9duit. Le but est de prendre un certain nombre d'informations trouv\u00e9es sur la page de la F1TV et de calculer des choses pour faire ressortir des tendances \u00e0 l'utilisateur. Cependant on reste sur des informations factuelles. Ce sont des infos d\u00e9duites que techniquement unn humain avec une bonne m\u00e9moire et fort en calcul mental pourrait faire. Mais la c'est fait automatiquement pour tous les pilotes et c'est affich\u00e9 de sorte \u00e0 faire ressortir les valeurs sp\u00e9ciales. Comme c'est un peu plus abstrait, je pense qu'un exemple vaut mieux que 1000 mots. \"Exemple de fen\u00eatre d'informations totalement calcul\u00e9es\" Ci dessus on peut voir un bon exemple. C'est une fen\u00eatre qui montre qui sont les pilotes les plus rapides et les moins rapides et qui montre la diff\u00e9rence de temps au tour. Cette information est totalement d\u00e9duite et n'est en aucun cas trouvable sur la F1TV mais elle n'est pas invent\u00e9e. Elle est simplement calcul\u00e9e. La formule est assez simple, je prend les cinq derniers temps au tour de tous les pilotes. Je fais une moyenne qui donne un temps. Et ensuite je trie les pilotes en fonction de ce temps et je n'affiche que les cinq plus rapide et les cinq plus lents. Ensuite il suffit de prendre le temps le plus rapide et faire une petite soustraction pour avoir l'\u00e9cart. C'est une stat assez int\u00e9ressante car elle lisse les diff\u00e9rences d'un tour \u00e0 l'autre et fait ressortir une tendance. On peut voir pour le grand prix de monaco 2023 par exemple, le moment ou les pneus pluie deviennent plus int\u00e9ressants que les pneus secs car on voit que les pilotes les plus rapides sont les pilotes de fond de grille qui ont chauss\u00e9 les pneus pluie en premier tandis que les plus lents sont les pilotes sur pneus secs vieux. Voici un bout de code qui s'occupe de faire les calculs : List <( int avg , string driverName )> averages = new List <( int avg , string driverName )>(); foreach ( DriverData driver in LiveDriverDataLogs [ LiveDriverDataLogs . Count - 1 ]) { //We want to recover the last 5 lap times List <( int lapTime , int lap )> laps = Storage . GetDriverLaptimes ( driver . Name , 5 ); if ( laps . Count > 0 ) { int avg = 0 ; foreach (( int lapTime , int lap ) lap in laps ) { avg += lap . lapTime ; } avg = avg / laps . Count ; averages . Add (( avg , driver . Name )); } } Affichage pr\u00e9dictif C'est ici que ca devient vraiment dommage, le projet a mannqu\u00e9 de temps pour impl\u00e9menter des affichages pr\u00e9dictifs mais le potentiel est la ! Un affichage pr\u00e9dictif est un affichage qui cr\u00e9e des informations \u00e0 partir des infos qu'il a mais qui fait plus qu'un calcul. Le but est de tenter de deviner quelque chose. Voici des exemples d'affichages pr\u00e9dictifs qui pourraient \u00eatre mis en place averc l'architecture actuelle du projet : Si un pilote a des pneus depuis plus de 20 tours et que son temps au tour est en chute libre depuis cinq tours. Alors ce pilote va peut-\u00eatre devoir s'arr\u00eater. Si un pilote tourne une seconde au tour plus vite que le pilote devant lui et que ce pilote est \u00e0 10 secondes devant, alors il devrait pouvoir le rattraper d'ici dix tours. Si un arr\u00eat au stand est en moyenne de 23 secondes, alors un pilote 3\u00e8me ressortirais potentiellement 7\u00e8me si il s'arr\u00eate maintenant. Tous ces exemples sont des mini algorythmes pr\u00e9dictifs qui pourraient \u00eatre impl\u00e9ment\u00e9s assez facilement dans l'architecture actuelle du projet et pourraient apporter une immense plus-value si ils sont bien param\u00eatr\u00e9s. On peut m\u00eame imaginer que l'algorythme se corrige tout seul si il voit qu'il a eu tort pour que les course suivante il puisse mieux s'en sortir. Les possibilit\u00e9es sont infinies ! Tests Alors la on arrive \u00e0 la GROSSE erreur de ce projet... Si je ne pouvais changer qu'une seule chose \u00e0 ma facon de faire le projet apr\u00e8s coup c'est ici que je ferai le changement. Les tests sont d'une importance absolument capitale mais si ils sont bien faits, c'est surtout un moyen ultra efficace de v\u00e9rifier que du nouveau code est performant et est beaucoup plus pratique \u00e0 utiliser. Je pense sans rire que j'aurais pu gagner plusieurs jours de travail si j'avais travaill\u00e9 diff\u00e9remment vis-a-vis des tests. Comment ca c'est pass\u00e9 D\u00e8s la cr\u00e9ation du planning pr\u00e9visionnel j'ai fait une erreur capitale. J'ai mis les tests en fin de developpement des features... Et je ne leur ai laiss\u00e9 que tr\u00e8s peu de temps tout en les mettant au milieu du chemin critique ce qui les rends particuli\u00e8rement vuln\u00e9rables si une t\u00e2che du chemin critique est retard\u00e9e. En fait dans ce projet je voulais surtout \u00e9viter de faire comme certains projets que l'on a pu avoir pendant notre formation. C'est \u00e0 dire que je ne voulais surtout pas oublier la doc. Alors j'ai agenc\u00e9 le projet pour commencer par les fondations de la doc, puis en incluant les p\u00e9riodes de programmation et entre ces derni\u00e8re ajouter des Tests dans les trous. Le soucis c'est que du coup les projets \u00e9taient un peu le dernier truc dont je devais me soucier ce qui a \u00e9t\u00e9 une tr\u00e8s mauvaise id\u00e9e. Je me suis retrouv\u00e9 \u00e0 devoir mordre sur les jours de tests car les t\u00e2ches de programmation mettaient plus de temps que pr\u00e9vu (qui elles-m\u00eame auraient p\u00fb \u00eatre plus courte avec une bonne utilisation des tests) et je mme suis retrouv\u00e9 \u00e0 passer outre les tests pour avancer sur le reste du projet. J'ai donc du en panique \u00e0 la toute fin du projet construire quelques tests \"unitaires\" dont l'utilit\u00e9 est tr\u00e8s limit\u00e9e car tout le travail a d\u00e9ja \u00e9t\u00e9 fait et que vu la complexit\u00e9 qu'a pris le projet, faire de vrais tests unitaires est devenu un peu trop compliqu\u00e9 pour valoir le coup. Les seuls tests \"unitaires\" (Je l'\u00e9cris entre quotes car ce ne sont pas vraiment des tests unitaires mais plut\u00f4t des tests tout courts car ils ne sont pas sp\u00e9cifiques) qu'il y a dans le projet final sont des tests exclusivement tourn\u00e9s sur l'OCR. Ils sont d\u00e9ja vraiment pratiques car cela me permet de tester d'autres algorythmes d'OCR et voir si les r\u00e9sultats sont meilleurs ou non mais c'est juste un peu trop tard quoi... Les tests unitaires que j'ai impl\u00e9ment\u00e9s sont un peu tous pareils au niveau du fonctionnement : On choisit une image dans une liste d'images pr\u00e9par\u00e9es qui sont scens\u00e9e repr\u00e9senter le type de donn\u00e9es rencontr\u00e9es par l'application en temps normal On lis le nom de l'image que j'ai mis manuellement en indiquant ce qui \u00e9tait marqu\u00e9 sur l'image On fait un coup d'OCR sur l'image et on compare ce r\u00e9sultat avec la valeur que l'on est scens\u00e9 retrouver En pratique on est sur un code de ce style : [TestMethod()] public void SectorOCR_Test () { string directory = @\"./../../TestImages/Sectors/\" ; foreach ( string file in Directory . GetFiles ( directory )) { Bitmap image = ( Bitmap ) Image . FromFile ( file ); DriverSectorWindow sectorsWindow = new DriverSectorWindow ( image , new Rectangle ( 0 , 0 , image . Width , image . Height ), 1 , true ); string [] paths = file . Split ( '/' ); string fileName = paths [ paths . Length - 1 ]; fileName = fileName . Replace ( \".png\" , \"\" ); int timeMS = ( int ) sectorsWindow . DecodePng (); string time = Reader . ConvertMsToTime ( timeMS ); string [] checkDigits = fileName . Split ( '_' ); string [] digitsToCheck = time . Split ( ':' ); if ( time == \"0:00:000\" ) { Assert . AreEqual ( 0 , Convert . ToInt32 ( checkDigits [ 0 ])); } else { //The ConvertMSToTime will always return three chars so we need to make the checkDigits be also three chars while ( checkDigits . Length != 3 ) checkDigits = new [] { \"0\" }. Concat ( checkDigits ). ToArray (); for ( int i = 0 ; i < checkDigits . Length ; i ++) { //We need to convert to int first because sometimes we have \"08\" and \"8\" and in string its not the same but in int it is Assert . AreEqual ( Convert . ToInt32 ( checkDigits [ i ]), Convert . ToInt32 ( digitsToCheck [ i ])); } } } } Tout le code suppl\u00e9mentaire qui ne fait pas partie des \u00e9tapes cit\u00e9es est juste la pour manipuler le format des r\u00e9sultats pour qu'il soit comparable. \"Exemple d'\u00e9chantillons pour les tests\" Ce qui est pratique avec cette approche, c'est qu'il est tr\u00e8s facile de rajouter des cas sp\u00e9cifiques et voir comment le programme les g\u00e8re. Si je vois qu'un certain nombre est souvent mal reconnu, je peux faire expr\u00e8s de le mettre dans le dossier et modifier mon code d'OCR jusqu'\u00e0 ce que le test passe. Si j'avais eu plus de temps, j'aurais s\u00fbrement pu ajouter de vrais tests unitaires qui testent des fonctions tr\u00e8s pr\u00e9cises. Par exemple, v\u00e9rifier que les diff\u00e9rents Windows sont bien appel\u00e9es et que les zones se cr\u00e9ent correctement ou m\u00eame plus simplement que la lecture du JSON au d\u00e9marrage marche bien. Il faut savoir que m\u00eame si je n'ai pas eu l'occasion d'\u00e9crire beaucoup de tests sous forme de code. Toute la phase de d\u00e9veloppement de l'OCR, j'ai pass\u00e9 plus d'une heure par jour \u00e0 analyser les r\u00e9sultats. Je gardais toutes les images des WINDOWS et je notais dans le nom du fichier ce que l'algorithme trouvait et je passais en revue manuellement les centaines de r\u00e9sultats pour isoler ceux qui posaient un probl\u00e8me. C'est comme \u00e7a que je me suis rendu compte par exemple que, avec cette police, les quatre et les 1 \u00e9taient souvent confondus. Donc m\u00eame si les tests automatis\u00e9s sont clairement insuffisants par rapport \u00e0 ce que j'aurais peut-\u00eatre d\u00fb faire, j'ai pass\u00e9 \u00e9norm\u00e9ment de temps \u00e0 tester mon application. Comment \u00e7'aurait d\u00fb se passer Si je devais refaire ce projet aujourd'hui, je pense que j'utiliserais un peu la m\u00eame technique que pour la doc. J'aurais mis les t\u00e2ches de Tests directement au d\u00e9but du projet et j'aurais d\u00e9termin\u00e9 le squelette de l'application par la m\u00eame occasion. Je pense que j'aurais mis trois jours pour \u00e9crire tous les tests dont j'aurais besoin et j'aurais fait une strat\u00e9gie de TDD (Test Driven Developpement) par ce que je pense que \u00e7a marcherait vraiment super bien sur ce type de projet. J'aurais pris, je pense, cinq une dizaine d'images compl\u00e8tes de la F1TV de plusieurs GP diff\u00e9rents et j'aurais mis toutes les fen\u00eatres d\u00e9coup\u00e9es dans des fichiers avec des tests comme ceux que j'ai faits pour ce projet. Et comme \u00e7a je saurai que mon algo est bon uniquement quand il aura r\u00e9ussi \u00e0 passer tous les tests. Cela r\u00e8glerait le souci que j'ai eu le plus : Me retrouver \u00e0 devoir changer l'OCR 5 fois par ce qu'\u00e0 chaque fois que je d\u00e9veloppe une nouvelle feature, je me rends compte d'une faiblesse, mon algorithme\u2026 Non seulement j'aurais eu beaucoup plus de facilit\u00e9 \u00e0 avancer sur le projet, mais en plus, je pense que cela m'aurait fait gagner \u00e9norm\u00e9ment de temps non seulement, car je n'ai plus \u00e0 tester tout \u00e0 la main, mais en plus par ce que \u00e7a veut dire que quand l'OCR passe les tests, je n'ai plus jamais \u00e0 m'en soucier. Le\u00e7ons Je pense que dans mes futurs projets, je mettrai les tests en d\u00e9but de projet plut\u00f4t qu'\u00e0 la fin et je ferai en sorte qu'ils fassent partie du chemin critique et que je ne puisse pas passer \u00e0 c\u00f4t\u00e9 sous pr\u00e9texte que \"Je n'ai pas le temps\". \u00c9crire des tests, ce n'est jamais marrant et c'est encore moins marrant quand ils nous emp\u00eachent d'avancer. Mais je suis convaincu qu'\u00e0 la fin, c'est un gain de temps et de s\u00e9r\u00e9nit\u00e9 incontournable. R\u00e9sum\u00e9 des difficult\u00e9s techniques Ici, je vais parler tr\u00e8s rapidement des difficult\u00e9s techniques rencontr\u00e9es. Si vous voulez tout savoir \u00e0 propos des difficult\u00e9s, vous pouvez aller lire le journal de bord. C'est aussi pour \u00e9viter de me r\u00e9p\u00e9ter par rapport aux explications des diff\u00e9rents points dans l'analyse organique. Je ne vais pas non plus parler des difficult\u00e9s rencontr\u00e9 avec des choses que je n'ai pas gard\u00e9es dans le programme final donc il est normal que vous vous disiez qu'il n'y a pas eu tant de difficult\u00e9s que \u00e7a. Browser Headless Il y avait plusieurs difficult\u00e9s techniques avec cette histoire de Browser Headless. D\u00e9j\u00e0 pouvoir lancer un browser headless et le contr\u00f4ler. C'est difficile, car il faut trouver la bonne librairie et ensuite, il faut trouver le bon ex\u00e9cutable de gecko Driver qui permette de faire fonctionner l'application m\u00eame si l'utilisateur n'a pas Firefox sur sa machine. Ensuite, la seconde difficult\u00e9 est celle de ne pas se faire chopper comme un bot par le site de la F1TV. Il faut savoir qu'\u00e0 ce jour, je n'ai toujours pas r\u00e9ussi \u00e0 faire croire \u00e0 la page de login de la F1TV que j'\u00e9tais un user normal en utilisant S\u00e9l\u00e9nium mais au moins maintenant, je peux acc\u00e9der aux vid\u00e9os tranquillement. Ce souci de ne pas pouvoir se connecter avec la page de login \u00e0 la plus grosse difficult\u00e9 technique de cette partie du projet : la connexion automatique. Pour me connecter \u00e0 la F1TV avec un browser headless la seule solution que j'ai trouv\u00e9e a \u00e9t\u00e9 d'utiliser des cookies. Et pour que l'utilisateur n'ait pas \u00e0 aller chercher les siens dans son navigateur, il a fallu trouver une technique pour aller les chercher directement sans lui demander son avis. Autre difficult\u00e9, comme on travaille avec un site web que l'on ne contr\u00f4le pas, il faut trouver un moyen de g\u00e9rer les erreurs et de r\u00e9essayer parfois et attendre quand il faut dans les cas o\u00f9 le chargement est long etc... Ensuite, apr\u00e8s tout \u00e7a, la derni\u00e8re difficult\u00e9 a \u00e9t\u00e9 de pouvoir contr\u00f4ler le Firefox Headless assez bien pour qu'il puisse non seulement naviguer les pages, mais aussi qu'il puisse cliquer sur des boutons qui ne s'affichent pas tout le temps. (Je ne vais pas mentionner la difficult\u00e9 que c'\u00e9tait de mettre le browser en 4K pour des raisons de sant\u00e9 mentale) OCR Les difficult\u00e9s ici sont dans un autre niveau. Chaque type de donn\u00e9e repr\u00e9sentait sa difficult\u00e9 \u00e0 lui tout seul, sans compter l'optimisation. Pour commencer, on a le texte pour les noms de pilotes. Il a fallu trouver un syst\u00e8me qui puisse reconnaitre le texte et qui puisse comparer le r\u00e9sultat avec les pilotes que l'on connait. Ensuite, il a fallu trouver un moyen de d\u00e9tecter la diff\u00e9rence entre les fen\u00eatres de DRS o\u00f9 il est ouvert ou ferm\u00e9. Il fallait \u00e9galement faire attention \u00e0 ne pas faire de faux positifs. Pour les temps par secteurs, il a fallu trouver des filtres qui permettent de bien diff\u00e9rencier les '1' et les '4' sans les confondre et il a aussi fallu trouver un moyen de filtrer l'image pour que dans le cas o\u00f9 le texte serait en couleur \u00e7a fonctionne quand m\u00eame. (Car oui, un filtre de nuances de gris ne marche pas super avec des couleurs sombres) Pour les pneus (le plus dur) il a fallu trouver un moyen de trouver sur toute la longueur de la zone la partie int\u00e9ressante. Ensuite, il a fallu trouver une technique pour savoir quel type de pneu c'est en fonction de la couleur moyenne et ensuite le plus dur a \u00e9t\u00e9 d'isoler le chiffre du dessin autour, car Tesseract n'aime pas les formes, le tout automatiquement. Pour les temps au tour, il a fallu trouver un moyen de ne pas confondre les ponctuations avec des chiffres tout en ne les ratant pas. Et il a fallu trouver un moyen de d\u00e9tecter quand in\u00e9vitablement cela arrive quand m\u00eame. Et la derni\u00e8re difficult\u00e9 (la plus p\u00e9nible) a \u00e9t\u00e9 de d\u00e9tecter les \u00e9carts entre les pilotes. Il a fallu trouver une fa\u00e7on de d\u00e9coder le texte en temps, mais aussi de faire tout un syst\u00e8me qui d\u00e9tecte et r\u00e8gle les cas ou un ':' a \u00e9t\u00e9 oubli\u00e9 ou confondu tout en ne sachant pas s'il \u00e9tait cens\u00e9 y en avoir \u00e0 la base, car les valeurs peuvent varier entre '1_23.657' et '0.452'. Stockage Pour ce qui est du stockage, la grande difficult\u00e9 a \u00e9t\u00e9 de savoir quand un pilote avait fini un tour parce que chaque pilote finit son tour \u00e0 un moment diff\u00e9rent. Il a \u00e9galement fallu trouver un moyen de savoir les donn\u00e9es d'un pilote \u00e9taient logiques. Une difficult\u00e9 qui n'a pas \u00e9t\u00e9 compl\u00e8tement d\u00e9pass\u00e9e est de savoir quand un pilote a fait un arr\u00eat aux stands, car la d\u00e9tection de l'\u00e2ge des pneus est plus que mauvaise. Voil\u00e0. Ce fut une petite liste non exhaustive de quelques difficult\u00e9s techniques que j'ai rencontr\u00e9es pendant ce projet. Optimisation du programme Ici, je vais parler des techniques que j'ai utilis\u00e9es pour r\u00e9duire le temps de traitement de chaque image de 50 secondes \u00e0 un peu moins de 3 sur le processeur de mon laptop. En effet, dans les premi\u00e8res versions du projet, traiter l'int\u00e9gralit\u00e9 d'une image pouvait prendre presque une minute. Ce qui est compliqu\u00e9 dans ce projet, c'est qu'il y a un certain nombre de choses que je ne contr\u00f4le pas. En utilisant Tesseract, je me retrouve avec des incompressibles. En imaginant que l'OCR sur une image prenne 300 ms, m\u00eame si j'avais 180 threads capables de faire cette t\u00e2che en m\u00eame temps, le temps de traitement sera toujours d'au moins 300 ms. Cr\u00e9er une instance de Tesseract prend \u00e9galement du temps. Ma mission n'est donc pas d'arriver \u00e0 des temps de quelques dizaines de millisecondes, mais plut\u00f4t de rajouter le moins de temps possible pendant le traitement et de tenter de faire le plus de choses possible en parall\u00e8le. Voici la liste des choses qui prennent du temps : Lancement du navigateur et navigation Cr\u00e9ation des instances de Tesseract Filtrage des images OCR Ce sont les quatre gros postes qui coutent le plus cher en ressources. Mais par chance, deux de ces postes ne sont appel\u00e9s qu'une seule fois au d\u00e9marrage, ce qui fait que ce n'est pas catastrophique s'ils prennent du temps. Tandis que l'OCR et le filtrage est fait \u00e0 chaque d\u00e9tection. Pour ce qui est du d\u00e9marrage, malheureusement, on ne peut pas faire grand-chose. Lancer le browser et naviguer \u00e0 travers la F1TV prend du temps, surtout si la connexion du client est mauvaise. Pour certaines actions, j'ai fait un syst\u00e8me qui essaie pendant 10 secondes de cliquer sur un bouton plut\u00f4t que d'attendre 10 secondes et cliquer pour tenter d'\u00e9conomiser un peu, mais malheureusement, c'est lent et on ne peut pas y faire grand-chose. Pour la g\u00e9n\u00e9ration des instances de Tesseract, c'est un peu pareil, mais pour d'autres raisons. Comme Tesseract n'est pas \"Thread Safe\" (Ce qui veut dire qu'il n'est pas parall\u00e9lisable), si on veut faire plusieurs reconnaissances \u00e0 la fois, il faut plusieurs instances de Tesseract load\u00e9es en m\u00e9moire. J'ai donc d\u00e9cid\u00e9, pour une question de simplicit\u00e9 et de performances, de faire en sorte que chaque fen\u00eatre de donn\u00e9e ou \"Window\" aie sa propre instance de Tesseract. Vous qui lisez ces lignes \u00eates peut-\u00eatre en train de vous dire \"Oulala mais \u00e7a doit beaucoup de m\u00e9moire son truc l\u00e0 \" et vous auriez parfaitement raison ! \"Consommation de m\u00e9moire peu apr\u00e8s avoir commenc\u00e9 la d\u00e9tection\" Ce programme consomme en effet une quantit\u00e9 absolument catastrophique de m\u00e9moire vive. Mais si je l'ai fait, c'est pour une bonne raison. Cela prend juste beaucoup trop de temps de cr\u00e9er une nouvelle instance \u00e0 chaque boucle de Tesseract et c'est encore plus long de faire toutes les op\u00e9rations d'OCR les unes apr\u00e8s les autres pour n'avoir qu'un seul Tesseract de load\u00e9. On peut parfois arriver \u00e0 des chiffres qui approchent les 4GB de RAM ce qui est absolument RIDICULE. Cependant, c'est un compromis que j'\u00e9tais pr\u00eat \u00e0 faire pour avoir une application qui soit plus rapide. Je suis absolument certain que cette solution et les autres solutions que j'ai trouv\u00e9es pour ce projet ne sont pas les meilleures ou les plus efficaces. Mais ce sont les solutions que j'ai trouv\u00e9 pour faire en sorte que le projet avance et fonctionne \u00e0 peu pr\u00e8s vite. Ensuite pour ce qui est de ce qui se passe \u00e0 chaque boucle, l\u00e0 le mot magique, c'est \"Parall\u00e8le\". Le traitement de toutes les zones est fait en m\u00eame temps. La structure du projet en zones, sous zones et fen\u00eatres de donn\u00e9es fait qu'il est assez facile de venir parall\u00e9liser le processus si on les impl\u00e9mente correctement. Diagramme qui montre comment les zones et fen\u00eatres interagissent On peut voir sur ce diagramme que la zone principale demande \u00e0 toutes les sous zones de d\u00e9coder leur contenu. Ces derni\u00e8res font l'exacte m\u00eame chose avec les fen\u00eatres de donn\u00e9es qui retournent chacune ce qu'elles contiennent apr\u00e8s un coup d'OCR et ensuite les zones recombinent les informations et les envoient \u00e0 la zone principale. Tout cela est tr\u00e8s bien, mais quel rapport avec la parall\u00e9lisation ? Eh bien, comme chaque zone de pilote est ind\u00e9pendante, on peut tout simplement faire une boucle for parall\u00e8le qui appelle toutes les zones pilotes. On passe de 15 \u00e0 20 secondes de traitement \u00e0 un peu plus de trois juste avec cette technique. Alors \u00e7a n'\u00e9tait pas simple \u00e0 impl\u00e9menter, car il a fallu programmer les zones de sorte qu'elles soient toutes ind\u00e9pendantes les unes des autres. Mais une fois que le travail en amont a \u00e9t\u00e9 effectu\u00e9, il est tr\u00e8s simple de parall\u00e9liser. Les filtres fonctionnent de la m\u00eame fa\u00e7on sauf que l\u00e0, on parall\u00e9lise le traitement de chaque ligne dans une image. L'impact est moindre qu'avec les zones, mais si on teste avec une machine assez puissante cela pourrait faire la diff\u00e9rence. Seul souci avec cette m\u00e9thode, cela veut dire que le processeur est particuli\u00e8rement sollicit\u00e9 '^^... \"Utilisation du processeur pendant le fonctionnement de l'application\" Mon laptop ne poss\u00e8de malheureusement que six coeurs ce qui limite pas mal la puissance de la paralellisation. Mais je suis convaincu qu'avec un CPU avec plus de coeurs on pourrait arriver \u00e0 d'encore meilleurs r\u00e9sultats. Mais cette utilisation du processeur a aussi un inconv\u00e9nient... \"Temp\u00e9ratures du laptop pendant le fonctionnement de l'application\" ; Donc si je veux commenter la F1 avec cet outil, note \u00e0 moi m\u00eame, je ne dois pas utiliser le laptop si je ne veux pas me cramer les doigts. Si je pouvais utiliser le GPU pour acc\u00e8l\u00e9rer le processus on pourrait peut-\u00eatre avoir de meilleurs r\u00e9sultats mais de ce que j'ai pu lire, l'OCR n'est pas sp\u00e9cialement un bon use case pour les GPU. Pour conclure, je dirais que ce projet est loin d'\u00eatre un exemple de performances et clairement, il y a des choix discutables qui ont \u00e9t\u00e9 faits et d'une mani\u00e8re g\u00e9n\u00e9rale, si je devais refaire tout le projet avec la performance en premier objectif, j'aurais s\u00fbrement fait diff\u00e9remment. Maintenant, avec le temps que j'ai eu, je suis d\u00e9j\u00e0 content d'avoir pu faire quelque chose qui fonctionne et qui ne prenne pas une minute \u00e0 traiter une image. Ethique du projet Ici, on va parler des questions \u00e9thiques de ce projet. En effet, il y a quelques petites choses qui peuvent soulever une question. Il y a deux questions qui reviennent presque \u00e0 chaque fois que je parle ou pr\u00e9sente mon projet : Utilisation abusive de la F1TV ? La F1TV est un service payant qui n'est pas forc\u00e9ment donn\u00e9 (m\u00eame si pas bien cher pour un utilisateur comme moi qui l'utilise plus d'une fois par semaine plusieurs heures). De ce fait, je ne peux pas rendre son acc\u00e8s plus facile ou faire fuiter des informations de courses que l'on ne peut se procurer que par son utilisation. Mais voil\u00e0 pourquoi je pense que mon utilisation n'est pas une utilisation abusive : L'application ne fonctionne que si l'utilisateur a un compte F1TV valide et qu'il s'est connect\u00e9 r\u00e9cemment sur sa machine. (Cela veut donc dire que je ne permets pas \u00e0 des utilisateurs de frauder) L'application ne partage aucune information sur le contenu de la F1TV avec l'ext\u00e9rieur. (On ne peut pas avoir acc\u00e8s \u00e0 des informations payantes sans abonnement) L'application ne simule qu'un seul utilisateur connect\u00e9 sur une vraie machine (Cela veut donc dire que je ne suis pas en train de faire un syst\u00e8me de bot qui regarde 45 flux en m\u00eame temps pour scrapper tout le site et/ou poser des probl\u00e8mes de DDOS) Les donn\u00e9es ne sont pas stock\u00e9es entre les sessions (cela veut dire que l'on ne repr\u00e9sente pas un risque de fuite de donn\u00e9es et on n'est pas un service qui vient scrapper le contenu pour alimenter une IA ou quoi que ce soit... pour l'instant...) En fait mon application fonctionne exactement comme si on avait une page ouverte avec la F1TV dessus et qu'un ami \u00e0 c\u00f4t\u00e9 de nous la regardait en prenant des notes pour nous aider \u00e0 suivre. Je ne vois donc pas le mal et je ne vois pas en quoi ce projet serait probl\u00e9matique sur ce point. Apr\u00e8s dans le futur, le but est clairement de conserver les infos trouv\u00e9es pour entrainer un algorithme de pr\u00e9diction et l\u00e0 peut-\u00eatre que cela pourrait poser plus de probl\u00e8mes, mais ce n'est pas le cas \u00e0 l'heure ou j'\u00e9cris ces lignes. R\u00e9cup\u00e9ration de cookies \u00e0 l'insu de l'utilisateur ? Alors l\u00e0, on est clairement sur le sujet un peu plus \u00e9pineux... Un peu de contexte d'abord : \u00c0 la base, je voulais que l'utilisateur entre ses identifiants dans mon application et ensuite que le navigateur les rentre dans la page de login automatiquement et qu'il puisse se connecter. Deux probl\u00e8mes \u00e0 cette solution : L'utilisateur doit avoir assez confiance en mon programme pour laisser ses identifiants en clair \u00e0 l'int\u00e9rieur. Il est extr\u00eamement difficile de bypass la protection contre les bots de la page de login de la F1TV. J'ai donc d\u00fb trouver une autre solution : Utiliser les cookies ! Le seul souci, c'est que cela voulait dire que l'utilisateur devait aller chercher lui-m\u00eame ses cookies dans le navigateur en utilisant F12 et qu'il devait \u00e0 nouveau me faire confiance pour que je n'en fasse rien. Je trouvais cette solution trop p\u00e9nible pour l'utilisateur alors, j'ai d\u00e9cid\u00e9 d'en trouver une autre. Utiliser les cookies MAIS, sans demander \u00e0 l'utilisateur. Pour faire simple, mon programme va directement d\u00e9coder les cookies encrypt\u00e9s dans la base de donn\u00e9e SQLITE de Chrome, va les stocker dans un CSV en clair et va laisser mon programme C# aller piocher ceux qui l'int\u00e9ressent. Soucis, mon programme a acc\u00e8s \u00e0 tous les cookies de l'utilisateur \u00e0 son insu, cela veut dire que je pourrais les utiliser \u00e0 des fins peu scrupuleuses. Et c'est la solution que j'ai d\u00e9cid\u00e9 de choisir, car elle permet \u00e0 l'utilisateur de ne rien avoir \u00e0 faire pour se connecter depuis l'application, mais cela veut aussi dire qu'il doit me faire confiance pour ne pas utiliser tous ces cookies pour mon utilisation personnelle. Sauf que contrairement aux autres solutions, il ne sait pas qu'il est en train de me faire confiance donc \u00e7a va. :D Non plus s\u00e9rieusement, oui, je pourrais faire n'importe quoi avec les cookies de l'utilisateur, non, je ne vais pas le faire, et non, je ne pr\u00e9vois jamais de le faire. Mais il est int\u00e9ressant de mentionner que mon application met en p\u00e9ril la s\u00e9curit\u00e9 des cookies de l'utilisateur et qu'il serait bien dans le futur de mettre un message explicatif au premier d\u00e9marrage ou dans l'installeur de l'application pour pr\u00e9venir l'utilisateur. Utilisation de Chat GPT \"Logo chat Gpt\" Cette ann\u00e9e, ChatGPT est venu s'installer dans la liste des outils que j'utilise presque quotidiennement pour avancer sur mes projets. J'ai utilis\u00e9 ChatGPT un certain nombre de fois pendant ce travail et je pense qu'il m'a fait gagner un certain nombre d'heures. En effet, dans certains cas tr\u00e8s pr\u00e9cis, ChatGPT est une ressource absolument g\u00e9niale. Je l'ai surtout utilis\u00e9 quand j'avais de soucis avec des librairies ou pour faire du troubleshooting. Ce que j'aime beaucoup avec ChatGPT c'est qu'il s'adapte \u00e0 ce qu'on lui donne. Par exemple, il m'est souvent arriv\u00e9 de vouloir utiliser des librairies comme Puppeteer sharp ou des exemples sont difficilement trouvables sur internet normalement. Et quand je voulais simplement faire fonctionner un exemple tr\u00e8s rapide, il a quasiment toujours pu me fournir le code minimum. Cependant, d\u00e8s que l'on arrive sur des cas encore plus pr\u00e9cis, on atteint assez vite les limites du syst\u00e8me. J'ai fr\u00e9quemment fait appel \u00e0 cet outil pour diagnostiquer du code, que ce soit pour d\u00e9tecter un souci ou m\u00eame plus juste pour voir si mon code avait du sens. En effet, si on donne une m\u00e9thode \u00e0 chatGPT, il va tenter de l'expliquer, et s'il n'y arrive pas, c'est g\u00e9n\u00e9ralement que les variables sont mal nomm\u00e9es ou qu'il y a un souci avec la logique du code. Et pour ce qui est de la d\u00e9tection des erreurs, l'exemple que je peux donner c'est quand je faisais des m\u00e9thodes asynchrones et parall\u00e8les, je pouvais lui donner la m\u00e9thode avec l'erreur que je ne comprends pas et il peut me donner cinq raisons de possibles soucis. Cependant, je pensais utiliser beaucoup plus ChatGpt mais \u00e0 la fin l'outil est assez limit\u00e9 et je ne l'utilisais que quand mes recherches internet \u00e9taient infructueuses. Le seul cas o\u00f9 il m'a un peu sauv\u00e9, c'est quand je travaillais avec Puppeteer et que j'essayais de r\u00e9gler un souci qui faisait que le programme plantait \u00e0 chaque fois que j'ouvrais une vid\u00e9o. Au bout de quelques heures de gal\u00e8re, il m'a juste propos\u00e9 d'utiliser une autre librairie comme s\u00e9l\u00e9nium et il m'a converti tout mon code puppeteer en code utilisable par Selenium, et m\u00eame si cela a demand\u00e9 un peu plus de travail que de copier-coller, pour finir, j'ai pu avoir quelque chose qui marchait et je n'aurais peut-\u00eatre pas eu le r\u00e9flexe ou l'envie de le faire si je n'avais pas utilis\u00e9 cet outil. En conclusion, certaines m\u00e9thodes de mon projet ont \u00e9t\u00e9 faites avec l'aide de ChatGPT mais c'est une minorit\u00e9 et je l'ai surtout utilis\u00e9 pour comprendre des erreurs et pour avoir des pistes \u00e0 explorer pour les fix. Rien de bien fou. Am\u00e9liorations futures Ici, je vais parler de deux types d'am\u00e9liorations. Les am\u00e9liorations \u00e0 court terme, que j'aurais pu faire si je n'avais pas perdu autant de temps sur certains probl\u00e8mes techniques ou si j'avais eu quelques semaines de plus pour travailler sur le projet. Et les id\u00e9es qui seraient plus compliqu\u00e9es \u00e0 mettre en place que je n'aurais jamais pu ajouter \u00e0 ce travail dans le temps imparti, mais qui sont maintenant possibles si je continue pendant quelques mois \u00e0 travailler sur le projet. Court terme Je vais commencer par les petites am\u00e9liorations. Chose que je regrette le plus, je dirai, c'est tout ce qui est affichage. J'aurais vraiment aim\u00e9 faire une magnifique interface, mais il m'a manqu\u00e9 de temps pour en faire une plus jolie et plus facile d'utilisation. Une am\u00e9lioration r\u00e9ellement n\u00e9cessaire serait d'am\u00e9liorer la d\u00e9tection des pneus pour qu'il soit possible de correctement d\u00e9tecter les arr\u00eats aux stands. En g\u00e9n\u00e9ral, si j'avais pu mettre plus de temps dans l'analyse des donn\u00e9es que je re\u00e7ois de la F1TV, j'aurais pu faire un syst\u00e8me plus efficace de d\u00e9tection de d\u00e9passements, car la version actuelle n'est vraiment pas bonne. Trouver un moyen de faire des erreurs plus pr\u00e9cises. En effet, maintenant, certaines erreurs ont des causes qui peuvent \u00eatre multiples (qui peuvent \u00eatre caus\u00e9es par un mauvais lien, ou une erreur de r\u00e9cup\u00e9ration des cookies ou m\u00eame juste de connexion internet). \u00c7a demanderait simplement un peu plus de temps pour qu'au lieu de retourner seulement une erreur, on tente de r\u00e9cup\u00e9rer plus d'infos pour la rendre plus sp\u00e9cifique. Et pour les am\u00e9liorations un peu plus concr\u00e8tes : Impl\u00e9menter plus d'affichages calcul\u00e9s. J'aurais aim\u00e9 ajouter des affichages comme le classement pond\u00e9r\u00e9 des pilotes en fonction des arr\u00eats aux stands. Cela demanderait juste un peu de temps et d'am\u00e9liorer la d\u00e9tection des pitstops. Impl\u00e9menter des affichages pr\u00e9dictifs simples. On pourrait imaginer des algorithmes simple qui pourraient tenter de pr\u00e9dire quand un pilote va en rattraper un autre ou quand un pilote va devoir s'arr\u00eater en fonction des temps aux tours. \u00c7a ne me demanderait pas de nouvelles technologies, mais simplement du temps pour mettre en place et tester les algorithmes. Faire un syst\u00e8me qui puisse tester les algorithmes pr\u00e9dictifs sur un panel de Grand Prix. Si l'\u00e9tape d'avant est faite, on peut facilement imaginer un bout de programme qui aille tester le programme sur diff\u00e9rents Grand Prix pour voir si les pr\u00e9dictions sont bonnes. Avoir une notion d'historique des courses pour avoir une page de comparaison des performances des \u00e9quipes. Par exemple, d\u00e9terminer quelle voiture est la plus rapide et comparer avec les autres circuits. On peut m\u00eame imaginer qu'apr\u00e8s plusieurs Grands Prix, on puisse tenter de d\u00e9terminer quelle \u00e9quipe est forte sur quel circuit. Avoir un syst\u00e8me qui permet de trouver automatiquement tous les liens de Grand Prix comme \u00e7a l'utilisateur n'aie plus besoin d'aller chercher un URL. Faire un installer pour qu'un utilisateur n'ait pas \u00e0 se taper la proc\u00e9dure d'installation (qui est assez p\u00e9nible) \u00e0 la main. Long terme L\u00e0, on va se pencher sur des features qui prendraient plus d'un mois \u00e0 mettre en place correctement. On pourrait imaginer un syst\u00e8me qui puisse regarder 50 Grand Prix et qui change automatiquement les variables des algorithmes en fonction de leurs performances (Un genre d'apprentissage machine rustique). On pourrait imaginer un syst\u00e8me qui puisse cr\u00e9er des infographies. Que ce soit au milieu de la course ou \u00e0 la fin, le programme pourrait nous g\u00e9n\u00e9rer des images avec une stat int\u00e9ressante (ex : x pilote a fait x d\u00e9passements ou x pilote gagnerait x points s'il finissait dans cette position, ce qui le ferait changer de position au classement g\u00e9n\u00e9ral). Si c'est bien fait, cela pourrait \u00eatre un outil extr\u00eamement pr\u00e9cieux, car je pourrais utiliser ces infographies dans mes commentaires. On pourrait avoir un syst\u00e8me qui donne une note de performance pour chaque pilote en fonction de ses performances et en fonction de sa voiture pour faire un genre de classement des pilotes. Il serait g\u00e9nial d'avoir une page de stats qui se souviennent de tous les anciens Grand Prix regard\u00e9s qui permettent d'afficher toutes les stats d'un pilote sur plusieurs courses. (Cela me permettrait, dans des moments o\u00f9 la course stagne un peu, de pouvoir prendre n'importe quel pilote et d'avoir des choses \u00e0 dire \u00e0 son sujet) On pourrait m\u00eame imaginer un syst\u00e8me qui utilise une base de donn\u00e9es sur un serveur Infomaniak et d\u00e9velopper une extension de navigateur qui me donne des infos importantes directement sur la page o\u00f9 je commente le Grand Prix ou qui au moins me fasse des notifications pour que je sache quand aller regarder, car un truc important s'est pass\u00e9. Je vais m'arr\u00eater l\u00e0 parce que les possibilit\u00e9s sont tout simplement infinies. \u00c0 partir du moment o\u00f9 je peux r\u00e9cup\u00e9rer toutes les informations de la F1TV de mani\u00e8re fiable, les champs des possibles sont ouverts et la seule limite est notre imagination. Il n'est pas impossible que je refasse une version de ce projet dans le futur qui me permette d'appliquer tout ce que j'ai appris pour le faire plus proprement (avec de la TDD par exemple) et qui me permette d'impl\u00e9menter toutes ces am\u00e9liorations et plus. Je pense vraiment que si je continue \u00e0 commenter pour le 20 minutes dans les ann\u00e9es qui viennent, cela pourrait \u00eatre int\u00e9ressant de d\u00e9velopper un outil du style qui pourrait grandement m'aider \u00e0 faire des commentaires de qualit\u00e9. Conclusion Bilan Je vais faire un petit bilan de ce travail. D\u00e9j\u00e0, je vous remercie chaleureusement d'avoir lu cette documentation (j'ai d\u00fb la relire en entier une ou deux fois, je sais que ce n'est pas facile). J'esp\u00e8re que j'ai pu parler de tout ce dont je voulais parler et que je l'ai fait de mani\u00e8re explicite et ais\u00e9 \u00e0 lire pour vous. J'ai r\u00e9ellement fait de mon mieux pour qu'elle soit la plus simple possible \u00e0 lire, mais c'est un exercice difficile dans un document de cette taille et je m'excuse des in\u00e9vitables erreurs et coquilles que vous aurez peut-\u00eatre remarqu\u00e9. Je dois avouer que je suis quand m\u00eame tr\u00e8s content d'arriver au bout de ce travail. J'ai vraiment aim\u00e9 cette exp\u00e9rience unique de pouvoir travailler \u00e0 100\u2009% sur un projet et voir de quoi je suis capable. Mais je suis aussi heureux d'arriver \u00e0 la fin, car je dois avouer que \u00e7a n'a pas \u00e9t\u00e9 simple tous les jours et que de travailler presque seul sur un projet si long n'est pas facile. Pour \u00eatre tout \u00e0 fait honn\u00eate, je suis quand m\u00eame fier de ce que j'ai fait (ce qui n'arrive pas souvent). C'est un projet qui est \u00e0 des ann\u00e9es lumi\u00e8res de la perfection, mais c'\u00e9tait mon id\u00e9e et en commen\u00e7ant le projet, je ne savais m\u00eame pas si j'allais y arriver. Certes le r\u00e9sultat n'est pas exactement comme je l'aurais r\u00eav\u00e9, mais il est concret et il fonctionne ! Il y a eu des moments ou en voyant la quantit\u00e9 de choses qu'il restait \u00e0 faire, je me sentais un peu d\u00e9courag\u00e9, mais je suis arriv\u00e9 au bout avec un projet fonctionnel et pour \u00e7a, je suis assez fier. Ce fut un projet difficile, surtout sur le plan de la r\u00e9solution de probl\u00e8mes. Chaque \u00e9tape du projet apportait une nouvelle probl\u00e9matique qu'il fallait r\u00e9soudre et si parfois, j'ai pu trouver des fa\u00e7ons \u00e9l\u00e9gantes de le faire, pour d'autres, il a fallu \u00eatre un peu plus cr\u00e9atif et moins regardant sur la m\u00e9thode, mais que sur le r\u00e9sultat. Je suis un peu frustr\u00e9 de rendre le projet alors que j'ai encore pleins d'id\u00e9es pour le rendre meilleur. Mais je suis content de rendre quelque chose qui fonctionne et qui est d\u00e9j\u00e0 techniquement utilisable sur le terrain. Ce projet m'a \u00e9galement appris pas mal de chose sur ma mani\u00e8re de travailler et sur la gestion de projet et je sais que tous mes futurs projets b\u00e9n\u00e9ficieront de ces apprentissages. R\u00e9sum\u00e9 des \u00e9preuves Ici, je vais tenter de r\u00e9sumer tr\u00e8s rapidement tout ce qui a d\u00fb se passer pour en arriver l\u00e0. Pour commencer, il a fallu trouver un moyen de r\u00e9cup\u00e9rer des images de la F1TV automatiquement. Pour ce faire, j'ai d\u00fb trouver une librairie qui me permette de contr\u00f4ler un navigateur Firefox. Il a ensuite fallu trouver un moyen de se connecter automatiquement, pour ce faire, j'ai d\u00fb \u00e9crire un bout de code Python qui est all\u00e9 chercher les cookies dans la base de donn\u00e9es de chrome. Ensuite, il a fallu r\u00e9ussir \u00e0 naviguer sur la page de la F1TV en tenant compte des chargements et de la protection anti bots. Puis finalement trouver un moyen de retourner une image en assez bonne r\u00e9solution. Avec ces images, il a ensuite fallu d\u00e9velopper un syst\u00e8me qui permette \u00e0 l'utilisateur d'indiquer au programme o\u00f9 se trouvaient les informations. Il a ensuite fallu faire un syst\u00e8me qui utilise ces informations pour d\u00e9couper l'image pour isoler les infos et les envoyer \u00e0 la partie reconnaissance. Cette partie reconnaissance a d\u00fb \u00eatre d\u00e9velopp\u00e9e de mani\u00e8re quasi unique pour chaque type d'information reconnue et en plus de la partie reconnaissance qui \u00e9tait d\u00e9j\u00e0 bien gal\u00e8re, il a fallu faire tout un syst\u00e8me qui puisse d\u00e9tecter les anomalies de reconnaissances pour \u00eatre s\u00fbr que les informations r\u00e9cup\u00e9r\u00e9es \u00e9taient bonnes. Apr\u00e8s tout \u00e7a, il a fallu faire en sorte que ces donn\u00e9es soient stock\u00e9es et affich\u00e9es correctement. Cr\u00e9er une fa\u00e7on de les afficher de mani\u00e8re utile et facile \u00e0 l'utilisateur. Et tout ce beau monde a d\u00fb \u00eatre optimis\u00e9 pour que l'application ne prenne pas une minute pour r\u00e9cup\u00e9rer des images et il a fallu raccorder ensembles toutes les parties du projet en un seul qui fonctionne correctement sans crasher. C'est un r\u00e9sum\u00e9 un peu barbare qui oublie \u00e9norm\u00e9ment de choses et qui ne parle pas des probl\u00e8mes rencontr\u00e9s, mais cela peut donner une vague id\u00e9e de la taille du projet et de pourquoi je suis d\u00e9j\u00e0 si fier, juste que tout fonctionne. Merci d'avoir lu cette documentation, j'esp\u00e8re qu'elle a \u00e9t\u00e9 instructive et je vous souhaite une excellente journ\u00e9e Notes de code Ici, je vais donner quelques petites infos qui pourraient vous \u00eatre utiles si vous d\u00e9cidez d'aller vous aventurer dans mon code source. Le programme n'est pas \u00e0 proprement parl\u00e9 un programme en MVC, le d\u00e9coupage g\u00e9n\u00e9ral suit quand m\u00eame cette philosophie, je vais donc les ranger de cette fa\u00e7on pour que \u00e7a soit plus simple pour vous de comprendre. Vues Comme le projet n'est pas un MVC parfait, les vues font quand m\u00eame quelques actions, mais les deux fichiers dont je vais parler ici sont \u00e0 au moins 90\u2009% juste de la vue Settings.cs Ce fichier contient tout le code pour contr\u00f4ler la vue des \"Settings\" qui est la vue qui se charge de la cr\u00e9ation et \u00e9dition des Presets. Si vous voulez changer le comportement de cette page, il faut \u00e9diter ce fichier. Cette vue utilise deux contr\u00f4leurs : F1TVEmulator ConfigurationTool Le premier pour pouvoir lancer une instance de Firefox qui permet de tester le syst\u00e8me, le second pour effectuer toutes les actions de cr\u00e9ation, modification ou de lecture des \"Presets\" Rien de bien fou \u00e0 dire sur ce fichier. La seule chose un peu bizarre est la gestion de la cr\u00e9ation des zones et des fen\u00eatres. Il y a tout un syst\u00e8me qui peut \u00eatre un peu bizarre \u00e0 premi\u00e8re vue qui sert \u00e0 d\u00e9tecter quand l'utilisateur clique sur l'image pour cr\u00e9er une zone. Je suis s\u00fbr qu'il existe une mani\u00e8re plus propre de le faire que celle que j'ai utilis\u00e9e, mais j'ai fait en sorte que cela fonctionne. Un truc qui serait bien \u00e0 ajouter dans le futur serait un moyen de visualiser au moins les points que l'on ajoute au fur et \u00e0 mesure plut\u00f4t que de tout voir \u00e0 la fin. Form1.cs Ce fichier contient tout le code pour contr\u00f4ler la vue principale. Elle se charge de lancer le navigateur et d'afficher toutes les donn\u00e9es r\u00e9cup\u00e9r\u00e9es ou stock\u00e9es. Cette vue utilise deux contr\u00f4leurs : F1TVEmulator DataWrapper Le premier pour contr\u00f4ler le navigateur (le lancer, le stopper, changer l'URL etc.) et le second pour acc\u00e9der \u00e0 des infos de la base de donn\u00e9e sans avoir \u00e0 l'appeler directement. Contr\u00f4leurs Ces classes ne sont pas des contr\u00f4leurs \u00e0 100\u2009%, car ils contiennent aussi un peu de calcul, etc. mais ont comme but principal de servir d'interface entre la vue et les donn\u00e9es. ConfigurationTool.cs Cette classe sert \u00e0 travailler avec la zone principale pour la contr\u00f4ler et \u00e0 contenir les m\u00e9thode qui servent \u00e0 la cr\u00e9ation de Presets. Les deux grosses m\u00e9thodes que cette classe contient sont : SaveToJson AutoCalibrate La premi\u00e8re sert tout simplement \u00e0 prendre la configuration actuelle et la sauvegarder en format JSON dans un fichier dans le dossier PRESETS. La seconde prend une zone, utilise de l'OCR pour localiser les endroits o\u00f9 il y a du texte et fait une calibration auto pour cr\u00e9er automatiquement les zones de pilotes. Les autres m\u00e9thodes sont juste des m\u00e9thodes qui appellent des m\u00e9thodes de mod\u00e8les et servent seulement d'interface. DataWrapper.cs Cette m\u00e9thode sert \u00e0 faire l'interm\u00e9diaire entre la form principale et le contr\u00f4ler \"Reader\" ainsi que la classe qui contr\u00f4le directement la base de donn\u00e9es. Elle interface avec ces deux classes : Reader Storage Reader est un genre d'hybride, mais qui se veut \u00eatre un genre de contr\u00f4ler de la lecture des donn\u00e9es sur les images et des fichiers JSON tandis que storage est le mod\u00e8le qui interagis directement avec la base de donn\u00e9es SQLITE. Cette classe contient des m\u00e9thodes qui auraient tr\u00e8s pu (et s\u00fbrement d\u00fbes) se retrouver directement dans la vue. La plupart des m\u00e9thodes sont l\u00e0 pour g\u00e9n\u00e9rer des contr\u00f4les qui contiennent des informations r\u00e9cup\u00e9r\u00e9es par la base de donn\u00e9es ou par l'OCR. Reader.cs Cette m\u00e9thode est un genre d'hybride. Elle contient des calculs, etc. mais son but est de servir d'interface entre le reste du programme et les zones/fen\u00eatres de donn\u00e9es. C'est cette m\u00e9thode qui va g\u00e9rer la classe Zone, qui va demander \u00e0 la classe zone de modifier, ajouter ou supprimer des fen\u00eatres etc. Elle contient aussi des m\u00e9thodes pour charger un \"Preset\" et dessiner sur les Images quand une vue en a besoin. Zone.cs Cette m\u00e9thode est clairement la plus discutable en tant que contr\u00f4leur, mais qui est en m\u00eame temps la plus proche. La raison est qu'elle peut \u00eatre deux choses. Une zone principale ou une zone de pilote. Dans le cas ou c'est une zone de pilote, c'est clairement une classe normale qui est utilis\u00e9e par un contr\u00f4leur et qui retourne des infos. Mais quand elle est utilis\u00e9e comme une zone principale, c'est l'orchestre de toutes les zones et fen\u00eatres. Dans ce dernier cas, c'est un interm\u00e9diaire entre les zones et fen\u00eatres. Elle ne sert qu'\u00e0 contr\u00f4ler des sous zones et leurs fen\u00eatres. Les seules m\u00e9thodes de cette classe servent \u00e0 demander des informations aux sous zones/fen\u00eatres. Il n'y a quasi aucun calculs. Mod\u00e8les L\u00e0, on va parler des classes \"classiques\" (lol). Il y a deux types de classes dans cette liste : Les classes normales Les classes enfants de Window.cs Les classes normales sont ind\u00e9pendantes et contiennent toutes des m\u00e9thodes et des infos tr\u00e8s diff\u00e9rentes, tandis que les classes d\u00e9riv\u00e9es de Window.cs ont toutes la m\u00eame structure et ont comme seul et unique but de retourner ce qui est marqu\u00e9 dans leur image. Il est donc normal que ces derni\u00e8res se ressemblent beaucoup. DriverDrsWindow.cs Cette classe est pr\u00e9vue pour contenir une image dans laquelle on peut voir l'\u00e9tat du DRS d'un pilote. La m\u00e9thode qu'elle utilise pour savoir si le pilote a activ\u00e9 son DRS ou non est d'utiliser la moyenne de couleur de son image. Elle retourne true ou false et elle contient elle-m\u00eame toutes les m\u00e9thodes qui sont n\u00e9cessaires pour donner une r\u00e9ponse (c'est un cas rare). DriverGapToLeaderWindow.cs Cette classe est pr\u00e9vue pour contenir une image dans laquelle on peut voir combien de temps s\u00e9pare le pilote actuel du pilote devant lui. La m\u00e9thode qu'elle utilise pour le savoir utilise de l'OCR et fait appel \u00e0 une m\u00e9thode contenue dans son parent Window. Elle est plut\u00f4t vide, car tout le traitement est d\u00e9port\u00e9 dans son parent. DriverLapTimeWindow.cs Cette classe est pr\u00e9vue pour contenir une image dans laquelle on peut voir quel \u00e9tait le dernier temps au tour enregistr\u00e9 du pilote. La m\u00e9thode qu'elle utilise pour le savoir utilise de l'OCR et fait appel \u00e0 une m\u00e9thode contenue dans son parent. Elle est plut\u00f4t vide, car tout le traitement est d\u00e9port\u00e9 vers son parent. DriverNameWindow.cs Cette classe est pr\u00e9vue pour contenir une image dans laquelle on peut voir le nom du pilote \u00e9crit en toutes lettres. La m\u00e9thode qu'elle utilise une partie d'OCR qui est d\u00e9port\u00e9e dans le parent et utilise aussi une m\u00e9thode appel\u00e9e IsADriver (qui aurait pu aussi \u00eatre d\u00e9port\u00e9e dans la page principale) qui v\u00e9rifie si le nom trouv\u00e9 existe. DriverPositionWindow.cs Cette classe est pr\u00e9vue pour contenir une image dans laquelle on peut voir la position d'un pilote. Cette m\u00e9thode est \u00e9galement un peu vide, car pour d\u00e9coder l'image le traitement est d\u00e9port\u00e9 dans son parent. DriverSectorWindow.cs Pareil que pour DriverPositionWindow.cs DriverTyresWindow.cs Cette classe est pr\u00e9vue pour contenir une image dans laquelle on peut voir l'infographique qui repr\u00e9sente le pneu du pilote. Cette m\u00e9thode est la seule fen\u00eatre int\u00e9ressante, car elle utilise du code d\u00e9port\u00e9 dans le parent, mais aussi une certaine proportion qu'elle contient elle-m\u00eame. Elle contient des m\u00e9thodes qui permettent par exemple de trouver la zone int\u00e9ressante dans l'image ou choisir quel pneu un pilote chausse en fonction de la couleur moyenne de l'image de la zone trouv\u00e9e. Pour toutes les zones de type Window, ce qui est vraiment int\u00e9ressant, vous le trouverez dans le parent. F1TVEmulator.cs F1TVEmulator est la classe qui s'occupe de tout ce qui concerne le navigateur Headless. Cette classe utilise la librairie Selenium et est la pour tout faire. Elle s'occupe aussi bien d'envoyer la requ\u00eate, de cliquer sur un bouton apr\u00e8s 34 secondes que de r\u00e9cup\u00e9rer les cookies qui permettront de se connecter ensuite. Voici les m\u00e9thodes qui s'occupent des cookies : StartCookieRecovering GetCookie Je d\u00e9conseille de modifier ces deux m\u00e9thodes. Elles ont une utilit\u00e9 tr\u00e8s claire et elles fonctionnent. (If its not broken dont fix it) Ce qui peut \u00eatre int\u00e9ressant en revanche, c'est la seule autre m\u00e9thode que cette classe propose sobrement intitul\u00e9e \"Start\". Cette m\u00e9thode est cod\u00e9e de mani\u00e8re totalement proc\u00e9durale et d\u00e9crit exactement toutes les actions \u00e0 faire \u00e0 partir du moment ou le navigateur est d\u00e9marr\u00e9, dans quel ordre et s'il faut les faire ou non. Si vous vouliez modifier quelque chose ici, je pense que la bonne id\u00e9e serait une meilleure gestion des erreurs. Pour le moment, si le programme n'arrive pas \u00e0 cliquer sur certains boutons, soit une erreur est lanc\u00e9e, soit on attend un peu avant de r\u00e9essayer. La vraie chose qui manque, c'est la raison pour laquelle ces boutons n'ont pas pu \u00eatre cliqu\u00e9s. Dans l'id\u00e9al, il faudrait ajouter un syst\u00e8me qui peut d\u00e9tecter la panne exacte pour que le message d'erreur soit plus personnalis\u00e9. Sinon c'est une m\u00e9thode qui marche plut\u00f4t bien et qui est faite compl\u00e8tement sur mesure pour l'utilisation de la F1TV. OcrImage.cs L\u00e0, on attaque les classes un peu plus \"bord\u00e9liques\". Cette classe regroupe toutes les actions de filtrage que l'on pourrait vouloir. Cette classe est pas mal utilis\u00e9e pour l'OCR. Il n'y a que deux choses \u00e0 savoir. Presque toutes les m\u00e9thodes de filtres sont g\u00e9n\u00e9riques et peuvent \u00eatre utilis\u00e9es \u00e0 peu pr\u00e8s n'importe o\u00f9 et n'importe quand et devraient toujours fonctionner tant qu'on leur fournit ce dont elles ont besoin (la plupart sont en statique). La seule m\u00e9thode qui va vous int\u00e9resser si vous voulez changer le comportement de l'OCR est la m\u00e9thode \"Enhance\". La m\u00e9thode enhance est un genre de mode d'emploi. Selon le contexte de l'image (si c'est une image qui vient d'une fen\u00eatre de DRS, de temps au tour, de pneu etc.) il y aura une combinaison de filtres diff\u00e9rente. Plusieurs m\u00e9thodes dans cette classe ne sont pas utilis\u00e9es, mais sont gard\u00e9es, car elles pourraient \u00eatre utiles. La plupart du temps, l'utilisation de ces filtres est d\u00e9cid\u00e9e avec des essais \u00e0 t\u00e2tons. Vous comprendrez donc vite que c'est mieux de garder sous le code des m\u00e9thodes car certaines combinaisons marchent mieux que d'autres. SqliteStorage.cs Cette classe est plut\u00f4t simple. Ce sont simplement toutes les m\u00e9thodes qui permettent de cr\u00e9er, \u00e9diter et acc\u00e9der \u00e0 la base de donn\u00e9es SQLITE. Vous y trouverez des m\u00e9thodes qui sont juste l\u00e0 pour cr\u00e9er la base comme d'autres plus sp\u00e9cifiques qui sont un peu plus sp\u00e9cifiques comme celles qui veulent r\u00e9cup\u00e9rer l'ID d'un pilote selon son nom ou celle qui veut r\u00e9cup\u00e9rer l'historique des temps autour d'un pilote. Rien de sp\u00e9cial \u00e0 dire sur cette classe. Window.cs Ahlala... je pense que c'est une des classes les plus longues de tout le projet. Du haut de ses presque 700 lignes, cette classe s'occupe de tout ce que les enfants fen\u00eatres pourraient avoir besoin. On retrouve des m\u00e9thodes pour calculer la diff\u00e9rence entre deux strings qui peut servir pour aider \u00e0 la reconnaissance de noms de pilotes ou bien une m\u00e9thode qui permet de convertir une image en tableau de bytes. La m\u00e9thode la plus grosse cependant et de loin est la m\u00e9thode GetTimeFromPng qui doit impl\u00e9menter un syst\u00e8me qui permet de d\u00e9tecter quand un temps est anormal et d\u00e9tecter si la raison est la mauvaise compr\u00e9hension d'une ponctuation ou le rajout d'un chiffre. Cela prend \u00e9norm\u00e9ment de place, car il y a beaucoup de cas particuliers et il a fallu tout coder \u00e0 la main. Je d\u00e9conseille \u00e0 qui que ce soit de lire cette m\u00e9thode, ainsi, elle pourrait causer de s\u00e9v\u00e8res dommages au cerveau humain. \u00c0 \u00e9crire, ce fut une horreur, \u00e0 comprendre, je n'ose pas imaginer. Sinon pas grand-chose de plus \u00e0 raconter. Structures Les classes de structures sont des classes qui ne contiennent que peu ou pas de traitement et qui sont simplement l\u00e0 pour contenir des informations. Elles sont pratiques, car elles permettent de rendre le code dans les autres classes beaucoup plus lisible et leur \u00e9viter d'utiliser des tuples bizarres. DriverData.cs Cette classe contient toutes les infos d'un pilote \u00e0 un moment donn\u00e9. On peut voir cette classe comme une classe contenant une ligne de la F1TV. Toutes les donn\u00e9es \u00e0 propos d'un pilote que l'on peut d\u00e9tecter en une d\u00e9tection sont stock\u00e9es l\u00e0-dedans. Il n'y a pas de notion d'historique ou quoi que ce soit. C'est simplement un moyen de stocker des donn\u00e9es de pilotes dans d'autres classes en ayant un nom logique et aider \u00e0 la lecture. Pas r\u00e9ellement de traitement. Ce fichier contient \u00e9galement un autre objet : Tyre. Cet objet contient les infos d'un pneu, rien de plus. Et voil\u00e0, ce fut un r\u00e9sum\u00e9 extr\u00eamement succinct de tous les fichiers .CS de l'application pour que vous sachiez ce que vous regardez quand vous irez voir dans la partie code source de la documentation. Normalement, il devrait y avoir aussi un certain nombre de commentaires dans ces fichiers pour expliquer certains choix un peu bizarres. C'est en anglais, mais pas de l'anglais tr\u00e8s difficile \u00e0 comprendre. Bonne chance ! Glossaire Vocabulaire F1 : DRS : Drag Reduction System. : Syst\u00e8me qui permet d'ouvrir l'aileron arri\u00e8re de la monoplace quand elle se trouve \u00e0 une seconde ou moins de la voiture devant elle. Cela permet de r\u00e9duire la train\u00e9e que la voiture subit et lui permet d'avoir un petit boost qui aide \u00e0 d\u00e9passer. Pitstop : Arr\u00eat aux stands : Pendant une course de F1, les pneus s'usent extr\u00eamement vite et tous les pilotes sont oblig\u00e9s de passer au moins une fois par les stands par course pour les changer. Et pour changer ces pneus, ils font un arr\u00eat aux stands que l'on appelle dans le milieu un Pitstop. Pneus Hard, Medium, Soft, Inter, Wet : Types de pneus de F1. Hard est un pneu qui ne s'use pas beaucoup, mais qui est lent, Soft est l'inverse et Medium est l'entre deux. Les pneus sont des outils strat\u00e9giques et il est tr\u00e8s important de savoir lequel chaque pilote utilise. Les pneus Inter et Wet sont des pneus pluies, l'Inter \u00e9tant pour les faibles pluies. Secteur : Section de circuit : Les circuits de F1 sont toujours d\u00e9coup\u00e9s en trois parties qui sont mesur\u00e9es s\u00e9par\u00e9ment et qui permettent une meilleure granularit\u00e9 dans l'estimation des r\u00e9sultats. On n'est pas oblig\u00e9 d'attendre la fin d'un tour pour savoir si un pilote est rapide ou non et on peut voir dans quelle partie du circuit, il est rapide ou lent. Monoplace : Voiture \u00e0 une seule place, terme utilis\u00e9 souvent pour d\u00e9crire les F1 dans le document. Grand Prix : Course officielle de Formule 1. \u00c9v\u00e9nement faisant partie du championnat du monde de Formule 1. Vocabulaire projet : Browser/navigateur Headless : Navigateur qui existe et fonctionne sans interface graphique OCR : Optical Character Recognition : Processus de reconnaissance de texte sur une image par un ordinateur TDD : Test Driver Developppement : D\u00e9veloppement avec pour objectif les test. Les tests sont \u00e9crits en amont et le but du d\u00e9veloppeur est simplement de les faire passer. MVC : Mod\u00e8le Vue Controlleur : Architecture de projet qui s\u00e9pare le traitement de l'information, son affichage et sa gestion. Preset : (dans ce projet) Set d'informations pr\u00e9par\u00e9es \u00e0 l'avance pour \u00eatre utilis\u00e9s ult\u00e9rieurement. DB : Data Base / Base de donn\u00e9e Cookie : Fichier cr\u00e9\u00e9 par un site internet stock\u00e9 sur la machine du client qui est utilis\u00e9 en g\u00e9n\u00e9ral pour conserver des informations de connexion m\u00eame apr\u00e8s la fermeture du navigateur. Window : Fen\u00eatre (dans ce projet) objet contenant une partie d'une image contenant une information pr\u00e9cise. Zone : (dans ce projet) objet contenant une partie d'une image qui peu \u00eatre sous divis\u00e9e en fen\u00eatres de donn\u00e9es. Wrapper : Code qui s'occupe de faire l'interface entre une librairie ou une classe pour rendre l'utilisation plus simple ou plus propre JSON : JavaScript Object Notation : Format de fichier qui permet de stocker des informations dans un format tr\u00e8s pr\u00e9cis AWS : Amazon Web Service : Service d'h\u00e9bergement d'Amazon User Agent : Signature num\u00e9rique du navigateur qui permet \u00e0 un site de d\u00e9tecter le type d'appareil et de navigateur connect\u00e9 (peut \u00eatre chang\u00e9 manuellement) CSV : Comma Separated Values : Format de fichier qui permet de stocker facilement des donn\u00e9es sous forme de tableau API : Application Programming Interface : Interface g\u00e9n\u00e9rique qui permet d'acc\u00e9der \u00e0 une ressource.","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. It's 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 lap times 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 useful 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 rubbish) 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 : \"Prototype 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":"Ici, je vais parler de l'\u00e9tat du projet \u00e0 la date du 12 Juin 2023. \u00c0 cette date, le projet est fonctionnel, mais comporte quelques diff\u00e9rences avec le cahier des charges original. Je vais expliquer non seulement ces diff\u00e9rences, mais aussi les raisons qui font qu'elles sont l\u00e0. Pour bien comprendre les diff\u00e9rences, il faut s'en r\u00e9f\u00e9rer au cahier des charges original. L'application doit \u00eatre \"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\". C'est \u00e7a la phrase la plus importante dans tout le CDC. Et je pense que tr\u00e8s honn\u00eatement, ce cahier des charges est rempli ! L'application actuellement disponible sur le repos GIT est une application de style compagnion Windows Forms qui r\u00e9cup\u00e8re les infos de la F1TV en temps r\u00e9el et elle affiche les informations qu'elle trouve importante. Donc, je dirais que l'objectif g\u00e9n\u00e9ral est rempli. Maintenant, c'est dans les d\u00e9tails que cela p\u00eache. Il est mentionn\u00e9 trois exemples d'infos \u00e0 suivre, je cite : \"Les pilotes qui sont proches (moins de 1-2 secondes qui sont ainsi 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\" r\u00e9sultats : Dans l'application, on peut effectivement voir les pilotes proches (Ce sont ceux qui sont \u00e0 moins de 3 secondes dans le version finale) Dans l'application, on peut aussi voir un affichage qui permet de voir les pilotes les plus rapides et les plus lents sur le circuit. On ne peut en revanche pas voir de classement pond\u00e9r\u00e9 selon les arr\u00eats aux stands, car l'application a du mal \u00e0 d\u00e9tecter des arr\u00eats. Ensuite pour ce qui est des pr\u00e9dictions, il n'y en a aucunes comme \u00e7a, c'est simple. Si on ne regarde que de tr\u00e8s loin le CDC et le projet final, on pourrait dire que c'est plut\u00f4t d\u00e9cevant puisqu'il manque beaucoup de choses comme les pr\u00e9dictions et certains affichages. On peut aussi se dire \u00e7a en comparant la maquette du CDC et le r\u00e9sultat final. \"Maquette originale du projet faite sur Figma\" \"Maquette originale du projet faite sur Figma\" Clairement, un \u0153il non avis\u00e9 pourrait \u00eatre tr\u00e8s d\u00e9\u00e7u et pourrait dire que c'est un \u00e9chec. Et moi je vais vous expliquer pourquoi, au contraire c'est un total succ\u00e8s. D\u00e9j\u00e0, la beaut\u00e9 de l'interface est tr\u00e8s difficile \u00e0 r\u00e9pliquer en Windows Forms et il faudrait plus d'une semaine de travail pour arriver \u00e0 quelque chose qui pourrait ressembler un tout petit peu \u00e0 la maquette. Ensuite, si on regarde bien, on a quand m\u00eame une application qui nous permet de suivre les informations de la course et qui calcule des choses \u00e0 notre place. C'est d\u00e9j\u00e0 une grosse plus-value par rapport \u00e0 la page Data de la F1TV. Et finalement, les pr\u00e9dictions, les affichages et le style, ce sont les choses les moins compliqu\u00e9es du projet. On ne se rend pas compte que pour simplement afficher les 20 pilotes dans le bon ordre, il faut \u00e9norm\u00e9ment de travail. Voici une petite repr\u00e9sentation graphique de la quantit\u00e9 de travail n\u00e9cessaire pour en arriver \u00e0 l'\u00e9tat actuel du projet : \"Graphique repr\u00e9sentant la quantit\u00e9 de travail requise\" Pour en arriver \u00e0 un affichage, il a fallu r\u00e9cup\u00e9rer automatiquement les images en utilisant un browser headless ce qui a pris un temps fou \u00e0 mettre en place et il a fallu surtout lire les informations que l'on recevait des images. J'ai pass\u00e9 presque 90\u2009% du temps de mon projet \u00e0 d\u00e9velopper des choses qui permettront ensuite de faire de l'affichage. Le fait qu'il y ait quoi que ce soit de logique qui s'affiche, cela veut dire que TOUT LE RESTE fonctionne ! Le moindre souci \u00e0 la r\u00e9cup\u00e9ration des images, ou surtout \u00e0 la reconnaissance de texte et de chiffres, et l'affichage est ruin\u00e9. Si j'avais pass\u00e9 ne serait-ce qu'une semaine de plus juste sur l'affichage, le r\u00e9sultat final n'aurait rien \u00e0 voir. Le souci, c'est simplement que le cahier des charges ne parle pas du tout du reste du projet et ne parle que du r\u00e9sultat final. Pour toutes ces raisons, je dirais que le CDC \u00e9tait trop superficiel, mais que l'application est conforme \u00e0 l'id\u00e9e g\u00e9n\u00e9rale de ce dernier et qu'il serait tr\u00e8s facile de la rendre parfaitement conforme maintenant que tout le travail de fond a \u00e9t\u00e9 fait et fonctionne et je pense donc que c'est un succ\u00e8s.","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, puisque 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 au niveau de la documentation et 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 peaufiner. 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 assur\u00e9ment 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":"Alors ! Ces lignes sont \u00e9crites dans les derniers jours du travail de dipl\u00f4me et j'ai des choses \u00e0 dire. Premi\u00e8rement, je suis plut\u00f4t content de mon estimation du travail. Je trouve que j'ai bien estim\u00e9 la quantit\u00e9 de travail et combien de temps les diff\u00e9rentes t\u00e2ches allaient prendre. La plupart des d\u00e9passements sont des impr\u00e9vus et/ou des allers et des retours entre d'autres t\u00e2ches. La raison pour laquelle je suis plut\u00f4t content de ma planification, c'est que malgr\u00e9 l'usine \u00e0 Gaz que repr\u00e9sente ce projet et le nombre de soucis que j'ai eu, j'ai quand m\u00eame pu arriver \u00e0 un projet qui fonctionne en suivant essentiellement fid\u00e8lement le planning. Une chose dont je suis assez fier, c'est la documentation. En ayant d\u00e9velopp\u00e9 le squelette de l'app d\u00e8s le d\u00e9but du projet, \u00e7a m'a permis d'avancer au fur et \u00e0 mesure du projet la conscience tranquille. Bon, c'est bien joli les fleurs, mais clairement, c'est loin d'\u00eatre parfait. Au moment de la planification, je n'avais pas pr\u00e9vu de faire des allers et des retours entre plusieurs t\u00e2ches. Dans le planning effectif, on peut voir qu'un jour, je suis sur la PT3 (Stockage) et la PT5 (regroupement des mini projets en un seul gros). J'aurais peut-\u00eatre d\u00fb inverser l'ordre. Mais il y a deux gros soucis dans mon planning : L'ordre des t\u00e2ches n'\u00e9tait pas bon (mais il a \u00e9t\u00e9 d\u00e9cid\u00e9 comme \u00e7a pour que les plus grosses difficult\u00e9s soient faites en premier) ce qui a cr\u00e9\u00e9 pas mal de soucis. Ex : L'\u00e9mulateur de la F1TV a \u00e9t\u00e9 fait tr\u00e8s tard et finalement les images r\u00e9cup\u00e9r\u00e9es n'\u00e9taient pas de la m\u00eame qualit\u00e9 que ce que j'avais pr\u00e9vu en d\u00e9veloppant l'OCR en premier. Les Tests ont \u00e9t\u00e9 n\u00e9glig\u00e9s et utilis\u00e9s comme des jours tampons. \u00c7a, c'est la plus grosse erreur de planning. Autant les autres sont p\u00e9nibles, etc. mais n'ont pas forc\u00e9ment compromis la bonne r\u00e9alisation du projet alors que les tests ont \u00e9t\u00e9 mal plac\u00e9s et ont finalement \u00e9t\u00e9 balay\u00e9s tandis que s'ils avaient \u00e9t\u00e9 mieux planifi\u00e9s \u00e7a ne serait pas arriv\u00e9. Solutions : L'ordre des t\u00e2ches a \u00e9t\u00e9 d\u00e9cid\u00e9 expr\u00e8s de cette fa\u00e7on pour \u00e9viter de prendre trop de risques. L'id\u00e9e \u00e9tait qu'en faisant le plus dur au d\u00e9but, je pourrai facilement changer le cahier des charges. J'ai envie de dire que j'aurais d\u00fb \u00eatre plus confiant, mais pour \u00eatre honn\u00eate, je pense que c'\u00e9tait un mal pour un bien. Je ne pense pas avoir \"bien\" fait, mais je pense que c'est une erreur qui \u00e9tait rentable pour mon niveau de stress dans le projet. Par contre, les Tests c'est tout simplement une erreur. J'en parle plus en d\u00e9tail dans la partie test de la documentation, mais je vais r\u00e9sumer un peu ici. La documentation a \u00e9t\u00e9 faite d\u00e8s le d\u00e9but du projet. J'ai mis en place le squelette pour qu'ensuite, il soit simple d'y ajouter au fur et \u00e0 mesure. J'aurais d\u00fb faire exactement pareil avec les tests. Si j'avais fait au moins le squelette des tests au d\u00e9but du projet, j'aurais pu beaucoup plus facilement en faire et cela m'aurait fait gagner un temps fou et j'aurais m\u00eame pu faire du TDD (Test Driven Developpement). Je suis persuad\u00e9 que cette b\u00eate erreur de planification m'a co\u00fbt\u00e9 tr\u00e8s cher, car ne pas avoir une bonne strat\u00e9gie de tests a d\u00fb me faire perdre un temps fou. Pour conclure, je suis content parce que j'ai r\u00e9ussi \u00e0 rendre un projet qui marche en suivant assez bien le planning, mais il y a des choses que je vais devoir changer dans mes prochains projets.","title":"Planning effectif et diff\u00e9rences"},{"location":"index.html#analyse-fonctionnelle","text":"Voir \"Manuel Utilisateur\" tout y est indiqu\u00e9","title":"Analyse fonctionnelle"},{"location":"index.html#analyse-organique","text":"","title":"Analyse Organique"},{"location":"index.html#outils-utilises","text":"","title":"Outils utilis\u00e9s"},{"location":"index.html#visual-studio-2022","text":"\"Logo de Visual Studio 2022\" C'est l'application que j'ai le plus utilis\u00e9, je pense. Visual Studio 2022 est l'IDE officiel de Microsoft pour coder en C#. C'est l'outil que j'utilise depuis maintenant six ans au CFPT et franchement, il fait tout ce que je pourrais vouloir. C'est aussi un outil pratique pour utiliser Windows Forms et faire des applications natives Windows. Pas grand-chose \u00e0 dire \u00e0 ce sujet \u00e0 part que c'est un outil qui marche bien et qui est gratuit si on prend la community \u00e9dition.","title":"Visual Studio 2022"},{"location":"index.html#visual-studio-code","text":"\"Logo de Visual Studio Code\" Cet outil est d\u00e9j\u00e0 un peu plus int\u00e9ressant. C'est le second outil que j'ai le plus utilis\u00e9. J'en ai surtout eu besoin pour \u00e9crire de la doc, mais aussi pour coder en python et pour contr\u00f4ler mkdocs. Visual Studio est un IDE absolument g\u00e9nial qui est tr\u00e8s puissant avec les bonnes extensions. Je l'utilise au quotidien pour tout ce qui est d\u00e9veloppement WEB, Mobile ou pour \u00e9diter des fichiers de configs pour mes drones ou imprimantes 3D. Je peux m\u00eame compiler le firmware pour ces derni\u00e8res en utilisant une extension faite pour. Les possibilit\u00e9s de customisation sont presque infinies et c'est un plaisir d'utiliser ce logiciel gratuit fourni par Microsoft, mais qui est am\u00e9lior\u00e9 constamment par des d\u00e9veloppeurs ind\u00e9pendants. Je conseille \u00e0 n'importe quel d\u00e9veloppeur de l'essayer \u00e0 moins qu'il soit uniquement sur C# ou il serait plus int\u00e9ressant d'utiliser visual studio 2022.","title":"Visual Studio Code"},{"location":"index.html#materialmkdocsmarkdown","text":"\"Logo de Mkdocs Materials\" Pendant ce projet, j'ai utilis\u00e9 exclusivement du Markdown avec l'aide de Mkdocs et Materials. Le choix de Markdown a \u00e9t\u00e9 plut\u00f4t simple, c'est une fa\u00e7on facile et efficace de cr\u00e9er de la documentation et on n'avait pas le choix de l'utiliser. On avait \u00e9galement l'obligation (Ou au moins un tr\u00e8s forte incitation) par nos professeurs d'utiliser mkdocs et materials pour que notre documentation ne soit pas simplement une liste de fichiers, mais un joli site dans lequel il est agr\u00e9able de chercher des informations. Mkdocs et Materials sont deux outils vraiment fantastiques, mais je dois avouer que je n'ai pas assez mis de temps pour apprendre tout leur potentiel. Pour moi ce sont simplement des outils et je veux qu'ils marchent. Je ne suis pas forc\u00e9ment du genre \u00e0 aller changer toutes les couleurs et polices pour avoir la doc parfaite, j'ai pr\u00e9f\u00e9r\u00e9 passer du temps sur mon app. Mais m\u00eame si ces outils offrent une customisation tr\u00e8s avanc\u00e9e, il est tr\u00e8s facile de cr\u00e9er un projet simple et j'aime beaucoup cette simplicit\u00e9. J'ai eu pas mal d'aide de la part de M. Briard pour impl\u00e9menter certaines features et je l'en remercie tr\u00e8s chaudement, car sans son aide ce document serait s\u00fbrement un peu moins commode \u00e0 lire (Oui oui \u00e7a aurait pu \u00eatre pire, je sais, c'est dur \u00e0 imaginer).","title":"Material/Mkdocs/Markdown"},{"location":"index.html#figma","text":"\"Logo de Figma\" Figma est l'outil que j'ai utilis\u00e9 pour cr\u00e9er mon poster et un certain nombre des diagrammes de cette documentation. J'utilise aussi cet outil d\u00e8s que je vais faire des maquettes de sites ou d'applications. D'ailleurs les maquettes dans le cahier des charges ont \u00e9t\u00e9 faites avec. C'est un outil en ligne parfaitement gratuit qui conserve tout dans le cloud. Franchement, je n'ai rien \u00e0 dire, je n'ai pas utilis\u00e9 plus de 15\u2009% des features que cet outil propose et je suis d\u00e9j\u00e0 conquis.","title":"Figma"},{"location":"index.html#technologies-utilisees","text":"Dans ce projet, diff\u00e9rents choix ont \u00e9t\u00e9 faits pour ce qui est des technologies. Certaines ont \u00e9t\u00e9 choisies, car elles \u00e9taient les plus simples, les plus pratiques, les plus efficaces ou encore les plus connues et donc ayant le meilleur support. Je vais tenter de r\u00e9sumer ici ces choix, mais je reviendrai sur la plupart d'entre eux plus tard quand j'explique ce que je fais avec.","title":"Technologies utilis\u00e9es"},{"location":"index.html#selenium","text":"\"Logo de s\u00e9l\u00e9nium\" Selenium est une librairie \u00e0 la base Node JS qui permet d'automatiser des actions sur un navigateur internet. Le but premier et je pense son utilisation premi\u00e8re et l'automatisation de tests pour des applications WEB. En effet, c'est un super outil pour simuler un user faisant un certain nombre d'actions sans apporter de variabilit\u00e9, ce qui fait de super test unitaires. Cependant, je pense que l'autre grande partie des utilisateurs de Selenium l'utilisent pour faire du \"Scrapping\". Et nous sommes un peu dans cette seconde cat\u00e9gorie. Le \"Scrapping\" c'est l'acte d'aller r\u00e9cup\u00e9rer des informations sur des pages web automatiquement pour alimenter sa propre base de donn\u00e9es. En effet, si on arrive \u00e0 passer les protections anti-bot, on peut facilement utiliser Selenium pour scraper tous les sites qui nous passent par la t\u00eate. Le cahier des charges que j'avais en t\u00eate en cherchant une technologie de contr\u00f4le de navigateur internet \u00e9tait le suivant : Simple Permettant de contr\u00f4ler un navigateur Headless (Voir chapitre \"Simuler un navigateur ?\") Permettant de contr\u00f4ler Firefox Ayant un wrapper C# Permettre de changer certaines choses comme les cookies en direct Permettre d'interagir avec les \u00e9l\u00e9ments d'une page Fonctionner Simple, car je ne voulais pas avoir \u00e0 passer trop de temps dessus (\u00e7a n'a pas bien vieilli lol...). Je voulais que l'on puisse utiliser Firefox parce qu'il n'impl\u00e9mente pas les m\u00eames s\u00e9curit\u00e9s que Chrome pour faire simple. J'avais besoin que la lib puisse contr\u00f4ler un Firefox HEADLESS comme je ne voulais pas avoir une page web ouverte sur mon ordi quand je commente, car c'est de l'espace utilis\u00e9 pour rien. J'avais besoin d'un wrapper C# puisque c'est le langage que j'utilise. Pour finir, j'avais besoin d'interagir avec les \u00e9l\u00e9ments de la page pour naviguer dessus et d'ins\u00e9rer des cookies pour me connecter sans avoir \u00e0 passer par le login de la F1TV qui est tr\u00e8s bon pour d\u00e9tecter les bots. Avec un cahier des charges pareil beaucoup de librairies ont \u00e9t\u00e9 abandonn\u00e9es. J'ai pu tester pleins de librairies C# qui arrivaient \u00e0 contr\u00f4ler un Chrome et m\u00eame pas mal qui arrivaient \u00e0 contr\u00f4ler un Chrome Headless. Mais le choix est tr\u00e8s vite restreint quand on veut pouvoir contr\u00f4ler Chrome OU Firefox. \u00c0 la base, mon choix, c'\u00e9tait port\u00e9 sur Puppeteer Sharp qui est une librairie qui se veut \u00eatre exactement ce que je veux. \"Logo de Pupeteer\" Je voulais utiliser cette librairie, car il y a des plugins qui sont tr\u00e8s orient\u00e9s scrapping, en effet, ils impl\u00e9mentent de nombreuses techniques pour permettre de mieux passer inaper\u00e7u par les syst\u00e8mes de d\u00e9tection de bots. Sur le papier, c'est la librairie parfaite qui correspond parfaitement au cahier des charges que je m'\u00e9tais fix\u00e9 et je pense que si j'utilisais un projet JS, elle le serait. Sauf qu'avec le wrapper C# j'ai eu un certain nombre de probl\u00e8mes : Toutes les versions de la librairie ne fonctionnaient pas. Il fallait faire des tests avec diff\u00e9rentes versions de la librairie et de ses d\u00e9pendances simplement pour faire lancer un browser. Et \u00e7a, c'est quand \u00e7a marchait, car il y avait des jours o\u00f9 des machines sur lesquelles je n'ai juste pas pu faire fonctionner la librairie. M\u00eame avec les techniques propos\u00e9es par les plugins \"Stealth\" je n'arrivais pas \u00e0 bypass les s\u00e9curit\u00e9s de la page de login de la F1TV. J'ai essay\u00e9 tout ce que j'ai pu trouver sur internet, mais on se fait toujours chopper d\u00e8s que l'on arrive sur la page. Et le pire de tous, impossible de faire fonctionner une vid\u00e9o. J'ai pu faire tout ce que je voulais faire finalement en passant par l'utillisation de cookies pour la connexion. Tout \u00e7a pour arriver au moment o\u00f9 il faut lancer la vid\u00e9o, et l\u00e0, crash. Impossible de faire fonctionner Puppeteer Sharp avec une vid\u00e9o. D\u00e8s qu'elle se lance, c'est un crash assur\u00e9 sans message d'erreur clair. Et le souci, c'est que le wrapper C# n'est pas vraiment bien support\u00e9 et que si c'est un bug de la lib, je ne risque pas de voir de fix avant un moment si ce n'est jamais. Pour toutes ces raisons, j'ai d\u00fb abandonner cette librairie, ce qui a \u00e9t\u00e9 tr\u00e8s dur, car j'avais pass\u00e9 beaucoup de temps dessus \u00e0 essayer de la faire marcher. Ensuite le choix de Selenium \u00e9tait plut\u00f4t simple, c'\u00e9tait la seule option restante. \u00c0 ce jour, je ne connais aucune autre librairie que Puppeteer ou Selenium qui puisse contr\u00f4ler un Firefox Headless en respectant mon cahier des charges et qui soit donc disponible depuis C#. Si je n'arrivais pas \u00e0 faire fonctionner Selenium, j'aurais d\u00fb abandonner l'id\u00e9e de simuler un navigateur tout simplement. Mais j'ai eu la chance que cette librairie fasse tout ce que je pouvais demander. C'est une super lib et m\u00eame si la version C# n'est vraiment pas bien document\u00e9e, la plupart des documentations de la version JS sont pertinentes pour la version C# m\u00eame si \u00e7a n'est pas la m\u00eame syntaxe. Pour r\u00e9sumer, j'ai choisi Puppeteer car c'\u00e9tait la seule option viable pour mon besoin. (Note : Par contre si je trouve la personne chez Mozilla ou puppeteer qui a d\u00e9cid\u00e9 d'hard coder la r\u00e9solution maximale du browser Headless que l'on peut override UNIQUEMENT en changeant les variables d'environnement de la machine ET DE NE LE DOCUMENTER QUASI NULLE PART JE JURE QUE CA VA TR\u00c8S MAL SE PASSER)","title":"Selenium"},{"location":"index.html#csharp","text":"\"Logo C#\" Je pense que c'est le choix le plus simple \u00e0 expliquer. C# est un langage de programmation orient\u00e9 objet relativement haut niveau qui a \u00e9t\u00e9 cr\u00e9\u00e9 par Microsoft et qui a comme cible le d\u00e9veloppement d'applications pour Windows. (On peut \u00e9videmment trouver des adaptations pour le faire tourner sur Linux, mais ce n'est pas vraiment le but du langage) En plus d'\u00eatre un superbe langage de programmation, c'est le langage que l'on apprend au CFPT informatique. C'est donc un langage avec lequel je suis beaucoup plus \u00e0 l'aise que pour d'autres langages comme le Python ou le JS. Mon but n'\u00e9tait pas de faire une application Web et je travaille sous Windows. Je savais que mon projet allait demander un minimum de programmation orient\u00e9e objet. J'ai ainsi imm\u00e9diatement pens\u00e9 \u00e0 utiliser C#. Cependant, j'aurais tr\u00e8s bien pu utiliser un langage comme python qui m'aurait clairement facilit\u00e9 la t\u00e2che avec des librairies bien plus fournies et plus souvent mises \u00e0 jour. Mais comme je ne suis pas du tout aussi \u00e0 l'aise avec, je pense que le C# \u00e9tait la meilleure option. Mes seuls regrets apr\u00e8s coup sont que je trouve les Windows Forms tr\u00e8s moches et qu'il est particuli\u00e8rement difficile de les rendre plus jolies et que les librairies disponibles en C# pour des sc\u00e9narios tr\u00e8s pr\u00e9cis ne sont pas au niveau de celles pour JS et pour Python. Cependant, si j'avais \u00e0 refaire le projet, je reprendrais C# je pense.","title":"CSharp"},{"location":"index.html#python","text":"\"Logo Python\" Alors ce choix-l\u00e0 est plus compliqu\u00e9 \u00e0 comprendre. Pour tout le projet, j'ai tent\u00e9 de garder le C# comme langage et de ne pas utiliser autre chose. Cependant, j'ai d\u00fb utiliser une seule fois le Python dans un cas tr\u00e8s pr\u00e9cis. Je n'aime vraiment pas coder en python de base et clairement, j'aurais pr\u00e9f\u00e9r\u00e9 ne pas l'utiliser, mais je n'avais pas le choix. Le besoin dans le cas du python \u00e9tait le suivant : J'avais besoin d'un moyen de r\u00e9cup\u00e9rer des strings et les d\u00e9coder avec une cl\u00e9 encod\u00e9e avec le syst\u00e8me propri\u00e9taire de Windows d'encodage. Le souci, c'est que j'avais avec le C# c'est que les m\u00e9thodes de d\u00e9cryptions ne fonctionnent pas pareil qu'en python et tous les exemples que je pouvais trouver \u00e9taient en python. J'ai essay\u00e9 pendant un sacr\u00e9 moment de faire fonctionner la d\u00e9cryptions en C# mais sans succ\u00e8s. J'ai donc directement utilis\u00e9 le python comme faisait toutes les personnes que je pouvais voir sur internet et je pense que \u00e7a n'est pas une mauvaise id\u00e9e. En effet, cela veut dire que si \u00e0 un moment Chrome est mis \u00e0 jour, je n'aurai pas besoin d'aller ouvrir tout le code source de mon projet pour tout recompiler, j'aurai simplement besoin de changer ce script. Un des avantages du Python est quand m\u00eame qu'il y a beaucoup plus de gens qui codent dessus, et pour ce genre d'utilisation tr\u00e8s sp\u00e9cifique, c'est plut\u00f4t pratique. Le seul probl\u00e8me, c'est que cela oblige l'utilisateur \u00e0 avoir python install\u00e9 sur sa machine et que sa version doit \u00eatre compatible... (les joies de python).","title":"Python\u202f?"},{"location":"index.html#firefox","text":"\"Logo Firefox Headless\" J'en parle d\u00e9j\u00e0 plus bas, mais le choix de navigateur est super important. D\u00e9j\u00e0 tous les navigateurs n'ont pas un mode Headless (sans t\u00eate, mieux expliqu\u00e9 dans la rubrique \"Simuler un navigateur ?\"). Par exemple, m\u00eame si Edge est maintenant bas\u00e9 sur Chromium, il n'existe pas de moyen de le faire tourner en Headless pour le moment. Autre souci, les librairies d'automatisation ne supportent pas tous les navigateurs. Par exemple, beaucoup supportent chrome, mais tr\u00e8s peu supportent Firefox ou Edge. Donc, il me fallait un navigateur qui puisse op\u00e9rer en Headless et qui soit support\u00e9 par plusieurs librairies d'automatisation. Il n'y a que Firefox et Chrome qui soient conformes \u00e0 ces exigences. (Je n'ai pas v\u00e9rifi\u00e9 pour TOUS les navigateurs. Peut-\u00eatre que les Opera GX ont aussi un mode headless super, mais je me suis concentr\u00e9 sur les navigateurs plus grand public). Chrome est support\u00e9 par plus de lib, mais le souci c'est que la F1TV utilise un lecteur de vid\u00e9o avec DRM (Plus d'infos l\u00e0-dessus dans la partie \"Simuler un navigateur\u202f?\") et donc le choix \u00e9tait simple. Il ne restait que Firefox.","title":"Firefox"},{"location":"index.html#tesseract","text":"Je pense que le choix le plus simple apr\u00e8s le C# fut l'utilisation de Tesseract. C'est tout simplement l'outil le plus utilis\u00e9 pour faire de l'OCR. \u00c0 la base, c'est une lib Python (Ah tiens encore ?) qui peut \u00eatre redoutablement efficace avec le bon dataset. Il existe d'autres outils, mais j'ai d\u00e9cid\u00e9 de prendre celui-l\u00e0 \u00e0 cause de son support juste incroyable et de son omnipr\u00e9sence dans la documentation OCR. En plus il est facile \u00e0 utiliser et je ne pense pas encore avoir fait le tour de tout son potentiel dans ce projet.","title":"Tesseract"},{"location":"index.html#fonctionnement-general","text":"Avant de passer \u00e0 l'explication de chaque partie du projet en d\u00e9tail, je pense qu'il est important de faire un petit point sur comment toutes les parties du projet s'emboitent et fonctionnement ensemble. Comme \u00e7a, quand vous lirez l'explication d'une \u00e9tape, vous serez conscient de \u00e0 quoi elle sert, et o\u00f9 elle s'inscrit dans le projet principal.","title":"Fonctionnement g\u00e9n\u00e9ral"},{"location":"index.html#les-briques-principales","text":"Voici trois grosses \u00e9tapes du projet. Pour rappel, ce sont des vulgarisations plut\u00f4t larges qui n'ont qu'un seul but, aider \u00e0 la compr\u00e9hension de ce qui vient par la suite.","title":"Les briques principales"},{"location":"index.html#recuperation-dimages","text":"\"Diagramme simplifi\u00e9 repr\u00e9sentant le processus de r\u00e9cup\u00e9ration des images\" Pour faire simple, on peut voir qu'il y a deux parties \u00e0 cette \u00e9tape. La premi\u00e8re en partant du haut repr\u00e9sente un script python qui va chercher des informations dans la base de donn\u00e9es de Chrome qui est en SQLite. Ces informations dans notre cas sont les cookies de connexion. Dans la seconde \u00e9tape, on peut voir que le programme utilise Selenium avec un navigateur Firefox Headless qui va aller communiquer avec la F1TV qui est le site web qui nous int\u00e9resse et qu'une des infos que l'on r\u00e9cup\u00e8re est une image de la page en format PNG que l'on envoie au programme C#. Ces deux parties sont li\u00e9es, car pour se connecter \u00e0 la F1TV Selenium a besoin des cookies de connexion r\u00e9cup\u00e9r\u00e9s par le programme Python. La premi\u00e8re partie est un processus qui n'est utilis\u00e9 qu'une seule fois au d\u00e9marrage tandis que la r\u00e9cup\u00e9ration d'images et en continu pendant toute la dur\u00e9e de l'utilisation de l'application.","title":"R\u00e9cup\u00e9ration d'images"},{"location":"index.html#ocr","text":"\"Diagramme simplifi\u00e9 repr\u00e9sentant le processus d'OCR\" ; On peut voir dans ce diagramme simplifi\u00e9 qu'avec l'aide de ce que contient le fichier \"Config.JSON\" on d\u00e9coupe l'image que l'on a r\u00e9cup\u00e9r\u00e9 au pr\u00e9alable en petits morceaux qui contiennent des informations. Ensuite, on prend cette image et on lui applique un filtre pour retirer le flou, la couleur, etc. Puis en utilisant de l'OCR (Optical Character Recognition) on en r\u00e9cup\u00e8re les informations sous forme de texte et on le renvoie dans le programme C# Dans cette partie explicative g\u00e9n\u00e9rale, on ne reviendra pas sur la cr\u00e9ation de ce fichier config. Pour plus d'infos \u00e0 son sujet, voir la rubrique (OCR/Fonctionnement g\u00e9n\u00e9ral)","title":"OCR"},{"location":"index.html#traitement-et-affichage","text":"\"Diagramme simplifi\u00e9 repr\u00e9sentant le processus de traitement et d'affichage\" On peut voir dans ce dernier mini diagramme simplifi\u00e9 qu'on prend les donn\u00e9es que l'on r\u00e9cup\u00e9rait de l'\u00e9tape pr\u00e9c\u00e9dente qui ne sont pas forc\u00e9ment toutes coh\u00e9rentes et qu'on les traite pour leur redonner du sens avant de les stocker dans une base de donn\u00e9es SQLITE. Ensuite cette m\u00eame base de donn\u00e9e fournis les infos n\u00e9cessaires pour des affichages (Ces affichages sont directement r\u00e9cup\u00e9r\u00e9s depuis le projet en cours de fonctionnement).","title":"Traitement et affichage"},{"location":"index.html#resume-du-fonctionnement-general","text":"\"Diagramme simplifi\u00e9 repr\u00e9sentant le processus global du projet\" Ce dernier diagramme est un sch\u00e9ma fait pour repr\u00e9senter de la mani\u00e8re la plus simple possible toutes les briques du projet et comment elles s'imbriquent ensemble. La repr\u00e9sentation est un peu diff\u00e9rente des trois autres diagrammes, car le but ici est surtout de montrer le chemin que fait la donn\u00e9e \u00e0 travers les couches. Toutes les parties du projet ne sont pas incluses, notamment la partie calibration dans un objectif de simplifier la lecture. Dans l'ordre, on peut voir que pour la partie r\u00e9cup\u00e9ration d'images, le python va r\u00e9cup\u00e9rer les cookies dans la base de donn\u00e9es chrome pour ensuite les retourner \u00e0 Selenium. Selenium va ensuite pouvoir lancer un navigateur (en l'occurrence Firefox) et utiliser les cookies r\u00e9cup\u00e9r\u00e9s pour aller sur la page de la F1TV qui va retourner un certain nombre d'infos \u00e0 Selenium. L'info qui nous int\u00e9resse le plus depuis Selenium ce sont les images de la page data de la F1Tv et ce sont elles que l'on va envoyer dans la partie LOAD du diagramme. Dans cette partie, on prend l'image de la F1TV et on la d\u00e9coupe selon les directives donn\u00e9es par le fichier Config.JSON (il renseigne les zones \u00e0 d\u00e9couper et ce qu'elles repr\u00e9sentent) et apr\u00e8s le d\u00e9coupage, on se retrouve avec une zone principale, vingt zones de pilotes et 9 fen\u00eatres de donn\u00e9es par zone de pilote donc 180 fen\u00eatres en tout. Ces fen\u00eatres sont ensuite envoy\u00e9es pour \u00eatre filtr\u00e9es (retirer le flou, mettre en \u00e9vidence les caract\u00e8res, en gros les pr\u00e9parer pour la reconnaissance) dans la partie OCR Dans cette partie, apr\u00e8s avoir filtr\u00e9 les images, on les envoie \u00e0 Tesseract pour qu'il nous retourne des r\u00e9sultats d'OCR. Ces r\u00e9sultats sont ce que Tesseract a trouv\u00e9 sur les images et ils sont retourn\u00e9s sous la forme de Data Pilote. Ex (Position : 1,Tour : 45, Temps au tour : 1:34.683, Pneus : Medium etc....) Finalement, ces donn\u00e9es ont envoy\u00e9es dans la partie traitement qui va faire des v\u00e9rifications d'usage pour s'assurer qu'elles sont correctes et quand c'est fait, tout est envoy\u00e9 dans une base de donn\u00e9es SQLite. On ne montre pas non plus dans ce diagramme la parte affichage des donn\u00e9es, car je ne trouve pas pertinent de l'inclure ici. Et voil\u00e0, c'est le fonctionnement tr\u00e8s g\u00e9n\u00e9ral et simplifi\u00e9 de l'application. Je vous invite \u00e0 continuer \u00e0 lire cette documentation pour des informations plus pr\u00e9cises \u00e0 propos de toutes ces \u00e9tapes. Bonne lecture !","title":"R\u00e9sum\u00e9 du fonctionnement g\u00e9n\u00e9ral"},{"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 mainmise sur les donn\u00e9es, ils peuvent ins\u00e9rer 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 Compagnon, mais bon) et c'est, je pense, la raison pour laquelle on ne voit aucune API publique qui permette de correctement se renseigner en donn\u00e9es en direct pendant un Grand Prix. Ils ont d\u00fb 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\" \u00c9videmment, 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 suffisantes et je ne peux pas leur faire confiance quand je commente. Ce qu'il m'aurait fallu, c'est une API publique et officielle qui me permette d'\u00eatre s\u00fbr 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\u00e8tement 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 juste 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 faciles \u00e0 avoir qu'elles sont IMPOSSIBLE \u00e0 avoir. Et c'est l\u00e0 que ce projet entre en jeu. Mais pour d\u00e9coder les donn\u00e9es d'une image, il faut d'abord ... (roulement de tambours) ... Avoir des images ! Et c'est l\u00e0 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\u00e9ration 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\u00e9der directement au flux sans passer par la plateforme internet et pouvoir prendre images \u00e0 volont\u00e9. Avoir tout simplement une page de la F1TV ouverte sur un \u00e9cran et prendre des screenshots \u00e0 intervalles 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\u00e9r\u00e9es de la m\u00eame mani\u00e8re que les diffusions en direct. Et que pour faire des Tests en direct, 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 peut 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 fa\u00e7on, 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\u00e8mes et permet une r\u00e9cup\u00e9ration quasi sans compromis.","title":"Comment faire ?"},{"location":"index.html#simuler-un-navigateur","text":"\"Navigateur Headless (sans t\u00eate)\" 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 (Interface Utilisateur). \"Chromium logo\" Cependant, La F1TV n'utilise pas simplement un player HTML5 basique. Elle utilise un service de streaming Bit Movin 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 versions 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 fa\u00e7on 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 appel\u00e9e \"PCP\" ou \"Protected Content Playback\" qui leur permet de bloquer au moins une partie des techniques de r\u00e9cup\u00e9ration du flux vid\u00e9o et audio. Cependant, Firefox de pas sa nature Open Source utilise \"Open H264\" pour lire ces m\u00eames flux soumis \u00e0 des DRM et Open H264 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\u2026 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\" 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 simplement 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\u00e9rer des images de la F1TV : D\u00e9marrer une instance de navigateur avec les bons arguments Ajouter les bons param\u00e8tres 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\u00e8tres Cliquer sur le menu d\u00e9roulant des r\u00e9solutions Trouver l'option 1080P et la s\u00e9lectionner Cliquer sur le bouton qui met la vid\u00e9o en plein \u00e9cran Prendre de screenshots \u00e0 intervalles r\u00e9guliers Pour faire toutes ces actions, on doit r\u00e9cup\u00e9rer 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 (); \u00c7a peut para\u00eetre plut\u00f4t simple dit comme \u00e7a et quand tout fonctionne \u00e7a l'est, mais la difficult\u00e9 vient du fait qu'\u00e0 peu pr\u00e8s n'importe 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. Parfois, il est aussi difficile de trouver un \u00e9l\u00e9ment selon son ID, sa classe, ou sa value. Par exemple, l'option qui permet de passer en 1080P peut avoir comme value 1080_9011456 ou 1080_9011200 si on refresh la page. Cela demande de passer par des expressions r\u00e9guli\u00e8res, ce qui n'est pas compliqu\u00e9 en soi, mais ce sont toutes ces petites choses qui rendent le processus long \u00e0 mettre en place. Il faut dire aussi que les sites ne sont pas forc\u00e9ment tr\u00e8s contents 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\u00e9rentes techniques pour passer outre ces restrictions comme : Changer son User Agent 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 m\u00e9thodes 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 h\u00e9berg\u00e9e autre part que le reste du site chez Amazon et qu'elle poss\u00e8de les meilleures s\u00e9curit\u00e9s que j'aie pu voir. Aucunes des m\u00e9thodes 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\u00e9der 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 personne qui utilise mon app. Alors \u00e9videmment \u00e7a n'est pas pour faire des b\u00eatises avec et c'est pour une \"bonne\" raison, mais bon quand m\u00eame \u00e7a peut faire bizarre comme \u00e7a. Je pense que vous savez d\u00e9j\u00e0 ce qu'est un Cookie, mais je vais malgr\u00e9 tout faire un petit point l\u00e0-dessus, car c'est important pour la suite. Quand on va sur un site internet et que l'on se connecte avec nos identifiants, nous sommes connect\u00e9s sur la session. Cependant, si on quitte le site ou que l'on ferme le navigateur, le site ne peut pas garder en m\u00e9moire que c'est bien vous quand le lendemain, vous retournez dessus. Pour palier \u00e0 cette limitation, on a invent\u00e9 cette chose magnifique (hem...) que sont les cookies ! Les cookies sont des petits fichiers qui sont stock\u00e9s dans votre navigateur et qui peuvent servir \u00e0 beaucoup de choses comme traquer votre activit\u00e9 sur internet et espionner un peu ou aussi par exemple, servir de jeton de connexion. L'id\u00e9e est que quand vous vous connectez sur le site avec vos identifiants, le site envoie un petit fichier dans votre navigateur qui va servir de jeton. Et donc lorsque vous reviendrez, le site pourra voir que vous avez le jeton et vous connectera automatiquement. \u00c7a peut para\u00eetre g\u00e9nial, et c'est effectivement bien pratique, cependant ce n'est pas sans risques. En effet, imaginons qu'un acteur malveillant parvienne \u00e0 s'emparer de ces petits fichiers, il pourrait ainsi facilement se faire passer pour vous. Alors un cookie expire \u00e0 un moment donn\u00e9 pour temp\u00e9rer les risques, mais ils sont toujours pr\u00e9sents. Dans notre cas, on peut vite comprendre pourquoi cela peut \u00eatre int\u00e9ressant de r\u00e9cup\u00e9rer ces cookies. En effet, si on peut mettre la main sur le jeton de connexion de l'utilisateur de notre application. On pourra se connecter automatiquement \u00e0 la F1TV et aller prendre des photos directement sans que l'utilisateur ait \u00e0 faire quoi que ce soit. Sauf que les cookies ne sont pas stock\u00e9s en clair comme \u00e7a. \u00c9videmment, Google Chrome a mis en place quelques techniques pour \u00e9viter que n'importe qui puisse s'amuser \u00e0 aller taper dans les cookies de la machine. Tous les cookies sont stock\u00e9s dans une base de donn\u00e9es SQLite avec les noms en clair et les valeurs sont encrypt\u00e9es en utilisant la m\u00e9thode AES 256 qui est une m\u00e9thode de cryptage tr\u00e8s utilis\u00e9e et efficace. Tellement efficace qu'il serait compl\u00e8tement inutile de tenter de les d\u00e9crypter en utilisant de la force brute pour trouver la valeur ou m\u00eame une attaque de dictionnaire ou quoi que ce soit. Si ces valeurs peuvent \u00eatre encod\u00e9es et d\u00e9cod\u00e9es en local sur la machine sans connexion internet, cela veut dire que la cl\u00e9 est stock\u00e9e sur la machine. Et si je peux mettre l\u00e0, mais sur cette cl\u00e9, alors je pourrai lire tous les cookies de la machine. Cette cl\u00e9 est stock\u00e9e dans les fichiers de Google Chrome sous Google\\Chrome\\User Data\\Local State . Et dans ce fichier, on peut trouver une liste de donn\u00e9es en cl\u00e9 valeurs et on peut trouver la cl\u00e9 sous os_crypt encrypted_key . On pourrait croire que l'on a d\u00e9j\u00e0 touch\u00e9 le jackpot, mais il reste encore une \u00e9tape. Cette cl\u00e9 est crypt\u00e9e en utilisant le syst\u00e8me d'encryption de Windows. Cette encryption est utilis\u00e9e pour emp\u00eacher des utilisateurs non connect\u00e9s d'acc\u00e9der \u00e0 certaines donn\u00e9es. Mais comme nous sommes connect\u00e9s, nous pouvons facilement utiliser les librairies de d\u00e9cryptions pour trouver la valeur de cette cl\u00e9. Et \u00e0 partir de l\u00e0, il suffit d'utiliser cette cl\u00e9 pour d\u00e9crypter tous les cookies de la machine pour aller chercher ceux qui nous int\u00e9ressent. Voici un exemple du code python qui permet d'aller chercher la cl\u00e9 d'encryption dans les fichiers de Google Chrome : 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 Python ?? Et oui j'ai choisit d'utiliser un srypt python pour aller chercher les cookies. Ce choix a \u00e9t\u00e9 fait pour trois raisons : Le python est un language que je n'aime pas particuli\u00e8rement mais qui poss\u00e8de un \u00e9ventail de librairies absolument fantastique. Et pour ce genre de choses qui demandent une constante mise \u00e0 jour des librairies et qui sont un peu niches le python est une option juste g\u00e9niale. Comme c'est une des parties qui est le plus suceptible de changer vu que Chrome change relativement souvent le syst\u00e8me de stockage des cookies. Dans une optique de facilit\u00e9 de maintenance, avoir un seul fichier qui concerne cette partie du projet et qui est dans un language que plus de gens maitrisent que C# est pratique. Je n'ai pas r\u00e9ussi \u00e0 trouver de librairies C# qui me donne des r\u00e9sultats identiques \u00e0 celles que j'utilise dans ce script python. Pour faire la liaison entre le C# et le python, j'appelle le script depuis mon C# et ensuite le python s'occupe de mettre tous les cookies dans un CSV qui est ensuite lu depuis le C#. Voici la partie python qui \u00e9crit dans le csv : 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\" ) Et la partie C# qui appelle le script et qui lit le CSV : 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 ; } Maintenant que l'on sait comment simuler et manipuler un navigateur internet, que l'on sait comment se connecter sur le compte F1TV de l'utilisateur sans qu'il n\u2019aie rien \u00e0 faire. On a tous les ingr\u00e9dients pour automatiquement r\u00e9cup\u00e9rer des images de la F1TV du Grand Prix que l'on souhaite.","title":"R\u00e9cup\u00e9rer les cookies ?"},{"location":"index.html#calibration","text":"Maintenant que l'on a des images de la page Data de la F1TV, on pourrait croire que c'est tout bon, on peut direct passer \u00e0 la partie OCR. Mais que nenni ! Le gros souci de l'OCR c'est que sa pr\u00e9cision est grandement r\u00e9duite d\u00e8s que l'on augmente la taille de la zone de recherche. M\u00eame simplement deux mots sur une image, si on les prend dans les images individuelles, on a de grandes chances de trouver quelque chose, mais si on les met les deux sur la m\u00eame et qu'on tente l'OCR, on va avoir de r\u00e9sultats bien moins bons. Et puis il faut aussi voir que selon les donn\u00e9es que je cherche, je ne peux pas faire le m\u00eame traitement. Par exemple, savoir si le DRS est allum\u00e9, savoir quels pneus chausse un pilote et depuis combien de tours et savoir quel est le temps de son dernier tour, ce sont des informations qui demandent des traitements qui n'ont rien \u00e0 voir. Il faut donc pouvoir dire au programme d'OCR ou se trouvent les informations et quelle est leur nature pour qu'il puisse les d\u00e9coder. Il faut donc faire une calibration qui puisse donner toutes les infos importantes, mais qui en m\u00eame temps soit facile \u00e0 utiliser, car un utilisateur doit \u00eatre capable de le faire assez facilement. Voici la liste des informations que l'on doit r\u00e9cup\u00e9rer : La liste des pilotes pr\u00e9sents sur le Grand Prix La position de la zone principale La position de chaque zone de pilote La position de toutes les Windows sur chaque zone de pilote Le but a \u00e9t\u00e9 de retirer le plus d'\u00e9tapes possibles \u00e0 l'utilisateur. Techniquement, j'aurais pu faire une version compl\u00e8tement manuelle, mais \u00e7a aurait pris trop de temps, alors il y a des syst\u00e8mes qui permettent de rendre cette t\u00e2che moins p\u00e9nible.","title":"Calibration"},{"location":"index.html#liste-des-pilotes","text":"Pour la liste des pilotes, j'ai pens\u00e9 \u00e0 utiliser une API externe pour avoir une liste dans laquelle on pourrait s\u00e9lectionner des noms de pilotes, sauf que j'ai abandonn\u00e9 l'id\u00e9e, car je trouvais que le projet avait d\u00e9j\u00e0 bien assez de points qui d\u00e9pendent de l'ext\u00e9rieur. Il y a donc une liste de pilotes dans laquelle on peut ajouter ou supprimer des noms de pilotes. L'id\u00e9al serait de mettre tous les pilotes de r\u00e9serve, comme \u00e7a si un pilote est malade sur une course, on n'a pas besoin de venir changer la liste.","title":"Liste des pilotes"},{"location":"index.html#zone-principale","text":"Pour la zone principale, c'est enti\u00e8rement manuel, on attend de l'utilisateur deux points x, y sur l'image pour ensuite avoir une id\u00e9e d'o\u00f9 est cens\u00e9 se trouver la zone. \"Exemple de zone principale\"","title":"Zone principale"},{"location":"index.html#zones-pilotes","text":"C'est l\u00e0 que \u00e7a devient int\u00e9ressant. L'utilisateur n'a pas besoin de faire quoi que ce soit pour que le programme sache o\u00f9 sont les zones des pilotes. J'aurais pu le faire manuellement en faisant choisir \u00e0 l'utilisateur de donner deux points qui correspondent \u00e0 la premi\u00e8re zone et extrapoler pour en avoir 20. Sauf que si l'utilisateur n'est pas pr\u00e9cis au pixel pr\u00e8s (et m\u00eame comme \u00e7a parfois) le vingti\u00e8me pilote se retrouve avec une zone compl\u00e8tement d\u00e9sax\u00e9e. L\u00e0, le programme va \"simplement\" effectuer une reconnaissance de texte sur toute l'image. Les r\u00e9sultats ne nous int\u00e9ressent pas vraiment, tout ce que l'on veut, c'est la position des textes. Avec les positions, il est facile de d\u00e9terminer o\u00f9 sont toutes les zones de pilotes et donc sans que l'utilisateur ait \u00e0 toucher quoi que ce soit, d\u00e8s qu'il a donn\u00e9 les infos pour la zone principale, les zones de pilotes sont d\u00e9termin\u00e9es. \"Exemple zone pilote\" Voici un exemple du code utilis\u00e9 pour trouver ou dessiner des zones de pilotes : 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":"Zones pilotes"},{"location":"index.html#windows-pilotes","text":"C'est ici que c'est le plus p\u00e9nible pour l'utilisateur, il doit s\u00e9lectionner manuellement les positions des fen\u00eatres de donn\u00e9es. Ensuite, d\u00e8s que l'utilisateur a donn\u00e9 une position pour chaque window, on applique les positions pour chaque zone de pilote. Il y a plusieurs types de windows et selon le type le traitement est diff\u00e9rent comme je l'ai dit plus t\u00f4t. Voici des exemples concrets : \"Exemple Window de pneus\" \"Exemple Window temps au tour\" \"Exemple window Drs\" Il est important que toutes ces zones soient transmises avec le plus de pr\u00e9cision possible pour que l'OCR puisse bien faire son boulot.","title":"Windows pilotes"},{"location":"index.html#stockage","text":"Ensuite, quand l'utilisateur a fini de configurer son flux, la configuration est stock\u00e9e pour qu'il puisse ensuite la r\u00e9utiliser pour tous les autres Grand Prix de l'ann\u00e9e. Le stockage est fait sous format JSON et est fait pour que le programme d'OCR puisse lire dedans toutes les infos n\u00e9cessaires. Cela fait des fichiers plut\u00f4t gros, mais je n'avais pas vraiment le choix. J'ai test\u00e9 une version avec seulement les infos de la premi\u00e8re zone de pilote, mais avec l'interpolation, les derniers pilotes se retrouvent avec des zones clairement pas \u00e0 la bonne taille. Voici un exemple de ce \u00e0 quoi ressemble le JSON final : { \"Main\" : { \"x\" : 36 , \"y\" : 343 , \"width\" : 3780 , \"height\" : 1454 , \"DriverZones\" : [ { \"name\" : \"Driver1\" , \"x\" : 0 , \"y\" : 1 , \"width\" : 3780 , \"height\" : 72 , \"Windows\" : [ { \"Position\" : { \"x\" : 45 , \"y\" : 3 , \"width\" : 76 , \"height\" : 65 }, \"GapToLeader\" : { \"x\" : 447 , \"y\" : 1 , \"width\" : 206 , \"height\" : 67 }, \"LapTime\" : { \"x\" : 863 , \"y\" : 3 , \"width\" : 229 , \"height\" : 65 }, \"DRS\" : { \"x\" : 1095 , \"y\" : 1 , \"width\" : 174 , \"height\" : 67 }, \"Tyres\" : { \"x\" : 1274 , \"y\" : 3 , \"width\" : 1448 , \"height\" : 62 }, \"Name\" : { \"x\" : 2724 , \"y\" : 3 , \"width\" : 361 , \"height\" : 65 }, \"Sector1\" : { \"x\" : 3088 , \"y\" : 1 , \"width\" : 239 , \"height\" : 65 }, \"Sector2\" : { \"x\" : 3314 , \"y\" : 4 , \"width\" : 190 , \"height\" : 62 }, \"Sector3\" : { \"x\" : 3493 , \"y\" : 1 , \"width\" : 198 , \"height\" : 67 } } ] }, { \"name\" : \"Driver2\" , \"x\" : 0 , \"y\" : 72 , \"width\" : 3780 , \"height\" : 72 , \"Windows\" : [ { \"Position\" : { \"x\" : 45 , \"y\" : 3 , \"width\" : 76 , \"height\" : 65 }, \"GapToLeader\" : { \"x\" : 447 , \"y\" : 1 , \"width\" : 206 , \"height\" : 67 }, \"LapTime\" : { \"x\" : 863 , \"y\" : 3 , \"width\" : 229 , \"height\" : 65 }, \"DRS\" : { \"x\" : 1095 , \"y\" : 1 , \"width\" : 174 , \"height\" : 67 }, \"Tyres\" : { \"x\" : 1274 , \"y\" : 3 , \"width\" : 1448 , \"height\" : 62 }, \"Name\" : { \"x\" : 2724 , \"y\" : 3 , \"width\" : 361 , \"height\" : 65 }, \"Sector1\" : { \"x\" : 3088 , \"y\" : 1 , \"width\" : 239 , \"height\" : 65 }, \"Sector2\" : { \"x\" : 3314 , \"y\" : 4 , \"width\" : 190 , \"height\" : 62 }, \"Sector3\" : { \"x\" : 3493 , \"y\" : 1 , \"width\" : 198 , \"height\" : 67 } } ] } [ O t her pilo ts ... ] ], \"Drivers\" : [ \"Perez\" , \"Verstappen\" , \"Alonso\" , \"Sainz\" , \"Russel\" , \"Gasly\" , \"Leclerc\" , \"Ocon\" , \"Hulkenberg\" , \"Bottas\" , \"Hamilton\" , \"Albon\" , \"Tsunoda\" , \"Zhou\" , \"Stroll\" , \"De Vries\" , \"Magnussen\" , \"Norris\" , \"Piastri\" , \"Sargeant\" ] } } Et avec tout \u00e7a. L'OCR peut d\u00e9marrer dans de bonnes conditions","title":"Stockage"},{"location":"index.html#ocr_1","text":"Maintenant qu'on a des images qui arrivent automatiquement et que l'on sait o\u00f9 se trouvent les informations sur ces dites images, 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 selon 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 \u00e7a, je dois expliquer certains concepts qui seront importants.","title":"OCR"},{"location":"index.html#fonctionnement-general_1","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 quatre zones contenant de l'information dans un format diff\u00e9rent. \"Zones principales\" Dans l'exemple ci-dessus, on peut voir trois zones, mais on aurait \u00e9galement pu comprendre la zone de position des pilotes autour du circuit pour faire 4. Ces quatre 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 \u00e7a \u00e0 impl\u00e9menter. J'ai utilis\u00e9 le mot \"Zone\" plus haut et \u00e7a 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 fen\u00eatres ainsi qu'une m\u00e9thode 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 m\u00e9thode qui permet de renvoyer ce qui peut \u00eatre d\u00e9cod\u00e9 sur son image. Les enfants peuvent aussi aller piocher dans les nombreuses m\u00e9thodes de r\u00e9cup\u00e9ration de donn\u00e9es contenues dans le parent Window. Il vaut mieux 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 biblioth\u00e8que. Si c'est une zone qui contient d'autres zones, c'est une biblioth\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 contiennent de l'information et sont stock\u00e9s dans des armoires et on y acc\u00e8de en allant dans la bonne biblioth\u00e8que et en allant dans la bonne armoire. Derni\u00e8res choses pour comprendre le diagramme : Il existe une Main Zone qui est une des quatre 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. Voil\u00e0 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 simplement les d\u00e9coder. Maintenant qu'on a une liste de diff\u00e9rents types de zones, on peut commencer \u00e0 chercher ce qu'il y a marqu\u00e9 dessus. Pour cela, il faut d'abord comprendre un petit peu comment l'OCR fonctionne et comment des libraires 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 qu'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 d\u00e9tecter les points communs entre les lettres pour cr\u00e9er un alpphabet. Par exemple, la matrice de la lettre 'H' donnerait un poids important \u00e0 des lignes verticales connect\u00e9es par une ligne centrale. Et si on fournit assez de donn\u00e9es de bonne qualit\u00e9 au mod\u00e8le, les matrices peuvent \u00eatre tr\u00e8s efficace \u00e0 d\u00e9tecter si une lettre est un H ou un M. Il y a pleins d'autres m\u00e9thodes 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 \u00e7a 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 inconsistants. Dans notre cas, le souci est que les chiffres et lettres sont beaucoup trop petits. Ils ne font parfois que 10 pixels de haut et cela fait qu'il n'est pas forc\u00e9ment ais\u00e9 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 qu'\u00e0 peu pr\u00e8s n'importe 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 antialiasing\" On voit que le 9 n'est pas clairement d\u00e9fini. En effet, on pourrait le comprendre comme : \"Premier exemple de contours\" Ou comme : \"Second exemple de contours\" Voire simplement comme : \"Exemple de contour g\u00e9n\u00e9reux\" Et on se rend bien compte que les performances de d\u00e9tection 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 m\u00e9thodes 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 \u00e0 plus de mal \u00e0 reconna\u00eetre, 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 puisque si une lettre pose un probl\u00e8me, 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 qu'il \u00e9tait souvent plus facile de trouver un noir sur blanc que blanc sur noir. Je ne suis pas s\u00fbr que cette \u00e9tape soit capitale cependant \"Texte invers\u00e9\" 2 : Je fais un Treshhold de 165, car avec moins le texte occasionnellement 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 m\u00e9thode 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 m\u00e9thodes pr\u00e9cises plus bas Voil\u00e0 pour ce qui est du post processing. Je ne dis pas que ce sont les meilleurs param\u00e8tres possibles, mais dans mes tests ce sont ceux qui ont le mieux march\u00e9s. Ce sont aussi les premi\u00e8res m\u00e9thodes 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 qu'on sait d\u00e9j\u00e0 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 qu'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 m\u00e9thode appel\u00e9e la distance de Levenshtein. Pour faire simple, c'est une m\u00e9thode qui va calculer les distances de lettres pour d\u00e9terminer entre des strings laquelle ressemble le plus \u00e0 une autre. Pour r\u00e9sumer le fonctionnement dans l'ordre : 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 m\u00e9thode en r\u00e9alit\u00e9 utilise juste la m\u00eame m\u00e9thode que celle qui va r\u00e9cup\u00e9rer le texte sur une image. Cependant, l\u00e0, 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 \u00e7a me soit arriv\u00e9 tr\u00e8s r\u00e9guli\u00e8rement et que \u00e7a me soit rest\u00e9 dans la gorge \u00e9videmment). L'avantage, c'est que cette m\u00e9thode ne demande m\u00eame pas de traitement de la donn\u00e9e en sortie de Tesseract. On esp\u00e8re simplement que le post traitement aura suffit. TEMPS : Cette m\u00e9thode 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 deux 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 \u00e7a. 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 \u00e9chelles 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\u00e8tement impr\u00e9visible. \u00c7a n'est simplement pas utilisable. Cette partie est un peu plus complexe, car si la d\u00e9tection n'est pas fiable, les chiffres sont juste 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 ambigu\u00eft\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 ressemblent beaucoup plus que les lettres, il faut tenter le plus possible de conserver leurs 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 m\u00e9thode 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 \u00c9rosion qui me permet de contrecarrer en partie les rondeurs ajout\u00e9es par la dilatation et retrouver des chiffres bien form\u00e9s. Pour l' \u00c9rosion 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 \u00c9rosion\" Explication des m\u00e9thodes pr\u00e9cises plus bas Et avec ce post processing, on retrouve de plut\u00f4t bons 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 qu'il va falloir transformer en millisecondes 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 d\u00e9tecter quand \u00e7a arriver. Je passe les d\u00e9tails du reste du nettoyage, car c'est vraiment du cas par cas, mais quand on a fini de nettoyer la chaine, on peut transformer les chaines de minutes, secondes et millisecondes en un total de millisecondes. 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 convertit le r\u00e9sultat en millisecondes","title":"Chiffres"},{"location":"index.html#les-chiffres-2","text":"Il faut savoir qu'avec la derni\u00e8re version de l'\u00e9mulateur (dont je vais parler un peu plus tard).","title":"les chiffres (2)"},{"location":"index.html#pneus","text":"L\u00e0, 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 pneu 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 \u00e7a : \"Exemple zone pneus 2\" Mais aussi \u00e0 \u00e7a : \"Exemple zone pneus 3\" Voire m\u00eame \u00e7a : \"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\u00e9ration 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 tours que le pilote a pass\u00e9 sur ce pneu. La couleur indique le type de pneu. S'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 m\u00e9thode 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 d\u00e9terminer 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\u00e8te\" Elle est automatiquement coup\u00e9e de cette fa\u00e7on : \"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 d\u00e9tection 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 m\u00e9thode qui calcule la diff\u00e9rence entre la couleur obtenue 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 o\u00f9 il n'y a pas beaucoup de couleur comme celui-l\u00e0 : \"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 d\u00e9terminer 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 n'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 sympathique de la lecture du chiffre. Vous saurez que Tesseract, en plus de d\u00e9tester les grandes images et les images avec des couleurs, d\u00e9teste \u00e9galement les formes dans une image. Ainsi dans notre cas, le round de couleur autour du chiffre, m\u00eame s'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 alors retirer le background AUTOUR du rond, et ensuite si on retire la couleur, il devrait rester le chiffre sur fond blanc. Pour ce 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 peut retirer les pixels qui ont une valeur dans un channel RGB plus haute qu'un certain seuil : \"Zone avec le reste des couleurs supprim\u00e9es\" Et l\u00e0, on a ce que l'on veut ! \u00c0 partir de l\u00e0, ce sont 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 m\u00e9thodes 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 \u00e7a, 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 d\u00fb utiliser diff\u00e9rentes m\u00e9thodes d'\u00e9dition d'image, que ce soit sous forme de filtres ou de modification de l'image directement. Voici un sommaire des m\u00e9thodes utilis\u00e9es et comment elles fonctionnent. Tresholding Cette m\u00e9thode 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'algorithme fonctionne sachant qu'il demande en entr\u00e9e la Bitmap que l'on veut modifier ainsi que la valeur de Treshold : On parcourt chaque pixel de l'image On convertit la couleur du pixel en une valeur de gris pour avoir la m\u00eame valeur en R,G et B (Formule utilis\u00e9e : gray = 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 algorithme pas forc\u00e9ment complexe, mais qui peut augmenter de mani\u00e8re titanesque les chances de r\u00e9ussir une OCR Resize Cette m\u00e9thode sert \u00e0 augmenter la r\u00e9solution d'une image pour am\u00e9liorer la pr\u00e9cision de l'algorithme 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 m\u00e9thode d'augmentation de la taille avec une simple interpolation. En effet, une augmentation de taille interpol\u00e9e ne va pas vraiment changer la r\u00e9solution, l'image sera toujours aussi pixelis\u00e9e, seulement, les pixels seront compos\u00e9s 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 d\u00e9tails, 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\u00e9j\u00e0 pr\u00e9sentes 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 d\u00e9tails. 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 \u00c9rosion Cette m\u00e9thode et la suivante font partie des m\u00e9thodes de transformation morphologiques. Ces m\u00e9thodes sont utilis\u00e9es pour accentuer les formes et les \u00e9paissir ou les r\u00e9duire et les affiner. Elles poss\u00e8dent l'aventage aussi de retirer le flou d'une image ce qui est tr\u00e8s pratique si utilis\u00e9 apr\u00e8s l'utilisation de m\u00e9thodes comme Resize . Je ne vais pas trop rentrer dans les d\u00e9tails de ces m\u00e9thodes, car leur fonctionnement est un peu plus lourd en math si on veut faire une v\u00e9ritable explication du pourquoi et du comment \u00e7a 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 m\u00e9thode est assez simple et est juste une m\u00e9thode 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 ambigu\u00ebs. 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 sophistiqu\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 d\u00e9tails, voir la d\u00e9tection de pneus. Il y a aussi d'autres m\u00e9thodes comme un filtre Gaussien ou Highlight countour que j'ai d\u00fb d\u00e9velopper, mais que je n'ai pas utilis\u00e9 donc je ne vais pas en parler ici.","title":"Filtres et m\u00e9thodes sur les images"},{"location":"index.html#petit-point-resolution","text":"Comme on peut l'imaginer, la r\u00e9solution est extr\u00eamement importante pour l'OCR. Et en avan\u00e7ant sur le projet de l'\u00e9mulateur, je me suis rendu compte qu'il \u00e9tait possible de r\u00e9cup\u00e9rer des images en 4K (Plut\u00f4t 1080 avec l'upscaling du lecteur). Cela est une superbe nouvelle car cela permet de simplifier \u00e9norm\u00e9ment le processing sur les diff\u00e9rentes windows. Quelques exemples pour se faire une id\u00e9e \"Echantillon 720P\" ; \"Echantillon 1080P\" \"Echantillon 4K\" Mais il faut savoir que gr\u00e2ce \u00e0 cette simplification, j'ai pu aussi cr\u00e9er d'autres m\u00e9thodes de filtrage pour certaines parties. Mais la simplification \u00e9tait obligatoire, car avec des images aussi grandes, il n'\u00e9tait simplement pas possible de venir appliquer les m\u00eames filtres car le temps de traitement serait beaucoup plus long. J'indique ces changements que apr\u00e8s l'explication d'avant car ce sont des changements un peu de derni\u00e8re minute et que la logique expliqu\u00e9e plus haut a \u00e9t\u00e9 tr\u00e8s importante pour le projet OCR m\u00eame si tout n'est plus forc\u00e9ment utilis\u00e9 maintenant que j'ai des images de meilleure qualit\u00e9. Dans la version actuellement disponible, la reconnaissance a \u00e9t\u00e9 simplifi\u00e9e sous cette forme : Le \"GapToLeader\" est d\u00e9cod\u00e9 avec un premier passage de Tresholding \u00e0 165 puis un Resize de 2 et une Dilatation de 1 pour retirer le flou Les \"Sectors\" sont d\u00e9cod\u00e9s en utilisant une toute nouvelle m\u00e9thode VanishOxyAction \u00e0 cause des couleurs parfois appliqu\u00e9es et ensuite simplement une methode de Tresholding de 150 pour rendre le r\u00e9sultat assez propre pour l'OCR. Le \"LapTime\" est d'abord pass\u00e9 par un Tresholding tr\u00e8s strict de 185 pour pr\u00e9parer la SobelEdgeDetection qui est \u00e9galement une nouvelle m\u00e9thode qu'il a \u00e9t\u00e9 possible d'utiliser gr\u00e2ce \u00e0 la simplification du reste des processus. Le \"Text\" est d\u00e9cod\u00e9 juste avec un tresholding de 165 d\u00e9sormais gr\u00e2ce \u00e0 l'image 4K. Les pneus ont leur propre traitement comme expliqu\u00e9 plus haut auquel on ajoute la Dilatation de 1. Comme on peut le voir, le traitement est pas mal plus simple, mais cela ne veut pas dire que les autres m\u00e9thodes que je n'utilise plus ne sont pas utiles. La reconnaissance n'est pas encore parfaite et je pense que leur utilisation pourrait aider \u00e0 am\u00e9liorer les r\u00e9sultats. (Et parfois ces anciennes m\u00e9thodes sont utiles dans les traitements personnalis\u00e9s des Windows elle m\u00eame comme par exemple les pneus qui utilisent la m\u00e9thode GrayScale pour isoler les couleurs) VanishOxyAction Cette m\u00e9thode est une m\u00e9thode plut\u00f4t simple, mais qui est importante. Elle se base beaucoup sur le code de la m\u00e9thode Grayscale et sur la m\u00e9thode Tresholding car elle essaie de regrouper le meilleur d\u00e8s deux en r\u00e9glant quelques soucis que ces derni\u00e8res cr\u00e9aient. Les soucis avec la m\u00e9thode grayscale c'est que quand le texte est de couleur (Ce qui arrive souvent pour les temps de secteurs) la m\u00e9thode GrayScale rend les couleurs dans une nuance de gris un peu trop sombre ce qui fait qu'ensuite la m\u00e9thode de Tresholding d\u00e9fonce tout. \"Exemple de secteur en couleur\" \"Exemple de secteur en grayscale\" L'id\u00e9e est alors de prendre pour chaque pixel et de garder uniquement la valeur de R, G ou B la plus haute et de mettre les deux autres canaux au m\u00eame niveau pour avoir une image blanchie qui puisse \u00eatre ensuite utilis\u00e9e avec la m\u00e9thode de Tresholding sans soucis. \"Exemple de secteur blanchi avec vanishoxyAction\" SobelEdgeDetection On pourrait se dire qu'avec ce genre de m\u00e9thode le tresholding est inutile ensuite, mais \u00e7a n'est pas le cas, car le tresholding sert ensuite pour rendre les contours plus ou moins agressif. Parce que m\u00eame si l'image ressemble \u00e0 une image binaris\u00e9e, il reste des nuances que le treshold va pouvoir utiliser. SobelEdgeDetection Cette m\u00e9thode est une m\u00e9thode assez classique que je n'ai pas design\u00e9 moi-m\u00eame alors, je ne vais pas trop m'\u00e9pancher dessus. En gros, on utilise une matrice et une formule math\u00e9matique pour redessiner une image et le r\u00e9sultat est une image avec des contours. Je ne l'ai utilis\u00e9 que pour les temps au tour, car ce sont les plus r\u00e9calcitrants. Cette m\u00e9thode a besoin d'une image pass\u00e9e en noir et blanc au pr\u00e9alable \u00e0 laquelle on applique ensuite les matrices de filtres. Et avec ces filtres ajout\u00e9s \u00e0 l'image, on peut ensuite calculer le \"Gradient\" pour cr\u00e9er les bords. Le seul souci de cette m\u00e9thode, c'est qu'elle est assez gourmande et qu'elle fournit des formes creuses d\u00fb \u00e0 la nature des matrices donn\u00e9es. Voici un exemple de ce dont cette m\u00e9thode est capable : Artefacts de la d\u00e9tection de bords de Sobel Apparemment l'OCR aime assez bien cette m\u00e9thode et elle permet de beaucoup moins souvent oublier les '.' ou ':'","title":"Petit point r\u00e9solution"},{"location":"index.html#traitement-des-donnees","text":"C'est bien gentil de recevoir des r\u00e9sultats de l'OCR, cependant on ne peut pas souvent les utiliser comme tels. En effet, les r\u00e9sultats ne sont pas tr\u00e8s constants et demandent d'\u00eatre v\u00e9rifi\u00e9s pour savoir s'ils doivent \u00eatre pris en compte. Le post traitement de ces donn\u00e9es d\u00e9pend compl\u00e8tement du contexte et donc il est diff\u00e9rent pour chaque type de window. Voici un floril\u00e8ge des diff\u00e9rents types de traitements : Traitement du nom de pilote Rien de plus que ce qui est d\u00e9j\u00e0 d\u00e9taill\u00e9 dans la partie OCR Traitement des pneus Pareil Traitement des temps L\u00e0, par contre, c'est int\u00e9ressant. Dans un monde parfait, je pourrais simplement prendre les r\u00e9sultats de l'OCR et les traiter directement. Mais comme nous vivons dans un monde ou la souffrance et la douleur sont les seules choses autoris\u00e9es, on ne peut pas. Le probl\u00e8me vient du fait que les temps que l'on peut trouver sur la F1TV sont encod\u00e9s avec des '.' et des ':' qui d\u00e9terminent les limites entre les chiffres qui d\u00e9signent les minutes, les secondes et les millisecondes. Et le souci avec ces s\u00e9parateurs, c'est qu'ils aiment bien mettre le chaos dans la reconnaissance. Quand ils ne sont pas compris comme des autres chiffres, ils sont parfois juste oubli\u00e9s ou pris en double, c'est un enfer. Il faut donc trouver un moyen de d\u00e9tecter quand cela arrive. Et je n'ai pas trouv\u00e9 de meilleurs moyens que de faire du cas par cas. Cela peut para\u00eetre simple quand on parle par exemple des secteurs. On sait qu'on attend deux chiffres avant un '.' et trois chiffres apr\u00e8s. Il est ainsi facile de voir que si je trouve six chiffres et pas de s\u00e9paration, le troisi\u00e8me est le s\u00e9parateur mal compris. Mais l'exemple qui d\u00e9truit vraiment tout, ce sont les \u00e9carts avec le leader. Autant un temps au tour, c'est toujours x:xx.xxx et un temps de secteur, c'est xx.xxx. Mais un \u00e9cart avec le leader \u00e7a peut \u00eatre 0.345 comme 1:12.345. Ce qui fait que lorsque je re\u00e7ois 121345 est-ce que c'est 12.345 ou 1:21.345...? Souvent, on peut quand m\u00eame d\u00e9duire, mais cela demande de pr\u00e9voir presque tous les cas limites, ce qui est assez p\u00e9nible. On pourrait se dire qu'il suffit de voir si la valeur est trop en dehors des normes. Le souci, c'est qu'il n'est pas impossible qu'un temps au tour ou un \u00e9cart prenne d'un coup une grosse diff\u00e9rence. Cela arrive m\u00eame assez souvent quand des pilotes sortent de la piste. \"Exemple temps au tour\" \"Exemple temps secteur\" Pour ce qui est du DRS et de la position des pilotes, il n'y a pas vraiment de traitement suppl\u00e9mentaire. Non pas, car la d\u00e9tection est parfaite, mais par ce que la d\u00e9tection ne peut pas rater de 200 fa\u00e7ons. Le DRS ne peut retourner que TRUE ou FALSE et la position du pilote est entre 1 et 20 compris. Le peu de nuance fait que ce sont des cas de figures qui ne demandent pas un traitement particulier au-del\u00e0 de l'OCR","title":"Traitement des donn\u00e9es"},{"location":"index.html#stockage-des-donnees","text":"Dans ce projet, le but n'est pas simplement de trouver les donn\u00e9es et les afficher. L'int\u00e9r\u00eat de les r\u00e9cup\u00e9rer est de pouvoir les comparer \u00e0 d'autres donn\u00e9es pr\u00e9c\u00e9dentes. Le vrai souci de la F1TV c'est justement que l'on ne peut pas facilement voir les \u00e9volutions. On ne peut voir que des \"photos\" de la situation actuelle de la course. Il faut donc garder en m\u00e9moire les diff\u00e9rentes choses qui se sont pass\u00e9es. Techniquement, on pourrait stocker ces donn\u00e9es dans de b\u00eates listes C#. Mais le souci avec \u00e7a, c'est que m\u00eame si des outils comme LinQ existent, \u00e7a n'est pas le plus pratique quand on veut faire des recherches complexes. Il faut aussi voir que si le projet dans sa forme actuelle aurait peut-\u00eatre pu se satisfaire de listes simples, le but est d'ensuite pouvoir construire sur ces bases pour faire des pr\u00e9dictions et des insertions de stats beaucoup plus int\u00e9ressantes qui demandent de faire des requ\u00eates complexe rapidement. Je me suis dit que la meilleure m\u00e9thode serait d'avoir une base de donn\u00e9e dans laquelle je peux faire des requ\u00eates SQL. Mais, comme je n'ai pas besoin de toutes les features de SQl et que je ne veux pas avoir \u00e0 g\u00e9rer un serveur de base de donn\u00e9e et tout ce qui va avec, je me suis dit qu'une bonne option serait d'utiliser SQLite. \"Logo SQLite\" ; SQLite est vraiment pratique, car cela me permet d'avoir une DB sans avoir de serveur, donc pas vraiment complexe ou quoi que ce soit, mais qui conserve les avantages de rapidit\u00e9 et d'utilisation de requ\u00eates SQL. J'ai cr\u00e9\u00e9 trois tables dans cette base de donn\u00e9e SQLite que voici :","title":"Stockage des donn\u00e9es"},{"location":"index.html#base-de-donnee","text":"Drivers Colonne Type de Data Description Tag ID INTEGER ID du pilote PRIMARY Name VARCHAR Nom du pilote NOT NULL Pitstops Colonne Type de Data Description Tag Lap INTEGER Tour durant lequel le Pitstop a \u00e9t\u00e9 effectu\u00e9 PRIMARY DriverID INTEGER Pilote qui a effectu\u00e9 le Pitstop PRIMARY Tyre VARCHAR Pneu chauss\u00e9 par le pilote NOT NULL Stats Colonne Type de Data Description Tag Lap INTEGER Tour durant lequel le Pitstop a \u00e9t\u00e9 effectu\u00e9 PRIMARY DriverID INTEGER Pilote qui concern\u00e9 PRIMARY Tyre VARCHAR Pneu chauss\u00e9 par le pilote NOT NULL LapTime INTEGER Temps au tour (MS) NOT NULL Sector1 INTEGER Temps du secteur 1 (MS) NOT NULL Sector2 INTEGER Temps du secteur 2 (MS) NOT NULL Sector3 INTEGER Temps du secteur 3 (MS) NOT NULL GapToLeader INTEGER Ecart avec le leader (MS) NOT NULL Position INTEGER Position pilote NOT NULL La table Drivers sert juste \u00e0 stocker les diff\u00e9rents noms de pilote pour qu'ils soient utilis\u00e9s dans le reste de la DB La table Pitstops n'est pas vraiment utilis\u00e9e dans l'\u00e9tat actuel du projet. Mais le but \u00e9tait de la remplir d\u00e8s que le programme d\u00e9tectait un arr\u00eat aux stands. Le but est ensuite de pouvoir construire un classement pond\u00e9r\u00e9 en fonction des arr\u00eats des diff\u00e9rents pilotes et d'afficher la stats tout le temps sur l'affichage principal. Elle n'est pas r\u00e9ellement utilis\u00e9e, car la d\u00e9tection de pitstop n'a pas pu \u00eatre compl\u00e9t\u00e9e. De par la nature des donn\u00e9es r\u00e9cup\u00e9r\u00e9es des pneus et des positions, c'est tr\u00e8s difficile de d\u00e9tecter avec pr\u00e9cision un arr\u00eat aux stands. La table Stats est la plus importante parce qu'elle contient toutes les informations concernant les pilotes \u00e0 chaque tour. L'id\u00e9e est qu'elle soit remplie \u00e0 chaque tour. Les infos ne sont pas cens\u00e9es \u00eatre les infos lives, mais plut\u00f4t juste une photo \u00e0 chaque tour de la situation de chaque pilote pour ensuite pouvoir faire des comparaisons tours par tours. Des donn\u00e9es comme le GapToLeader peuvent \u00e9voluer pendant le tour, mais on s'en fiche. Ce qui compte vraiment, c'est le temps au tour et les secteurs ainsi que les pneus.","title":"Base de donn\u00e9e"},{"location":"index.html#quand-remplir-la-base","text":"Dans ce projet, il y a deux types d'information. Les informations lives qui sont stock\u00e9s dans des listes et les informations long terme qui sont stock\u00e9es dans la DB. \u00c0 chaque it\u00e9ration de l'OCR, les donn\u00e9es r\u00e9cup\u00e9r\u00e9es sont stock\u00e9es dans une liste de DRIVERDATA. Les DRIVERDATA sont des structures de donn\u00e9es qui contiennent toutes les infos d'un pilote \u00e0 un instant T. Elles peuvent \u00eatre incompl\u00e8tes et sont juste l\u00e0 pour faire de petits calculs et d\u00e9terminer quand ins\u00e9rer des donn\u00e9es permanentes. Ce qui nous am\u00e8ne au moment int\u00e9ressant. Comment on d\u00e9termine quand il est int\u00e9ressant d'ins\u00e9rer des informations dans la base de donn\u00e9es. Il y a deux cas de figure ou on pourrait vouloir ins\u00e9rer des infos :","title":"Quand remplir la base ?"},{"location":"index.html#quand-un-pilote-a-fini-un-tour","text":"En effet, j'ai estim\u00e9 que les seuls moments o\u00f9 on veut garder une photo de la situation du pilote, c'est, car il passe d'un tour \u00e0 l'autre. Le raisonnement est le suivant : On ne veut pas conserver TOUTES les donn\u00e9es parce que si on prend une photo toutes les trois secondes, la majorit\u00e9 des informations seront redondantes avec les pr\u00e9c\u00e9dentes. Mais en m\u00eame temps, il ne faut pas rater des changements importants de donn\u00e9es. Les seules donn\u00e9es qui changent entre deux passages de l'OCR sont les \u00e9carts entre les pilotes et de temps en temps un nouveau secteur s'affiche. Alors que d'un tour \u00e0 l'autre presque toutes les informations changent. Et on ne perd que les l\u00e9g\u00e8res fluctuations des \u00e9carts entre les pilotes. J'ai donc d\u00e9cid\u00e9 de conserver une photo par tour. Mais c'est bien joli sauf qu'il reste une difficult\u00e9 : Comment savoir qu'un pilote a fait son tour ? Cela peut para\u00eetre simple comme question, mais elle est plus difficile qu'il n'y parait. Il faut savoir qu'en F1 un pilote peut \u00eatre dans son 26\u1d49 tour pendant qu'un autre en est \u00e0 son 24\u1d49. Chaque pilote a sa propre course et au fur et \u00e0 mesure que les \u00e9carts se creusent, il peut y avoir un tour voir plusieurs d'\u00e9cart entre la queue de course et les premiers pilotes. Ensuite, il faut savoir qu'il n'est pas marqu\u00e9 sur la f1TV dans quel tour chaque pilote est. Il faut donc le d\u00e9duire en fonction des Data. Voici le code le if qui d\u00e9tecte un nouveau tour if ( DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 1 ]. Sector3 != 0 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. Sector3 == 0 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. Position != - 1 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 1 ]. Position != - 1 ) { //Do stuff } DriverDataLogs est une liste de liste d'informations de pilotes. Chaque DriverDataLogs repr\u00e9sente les 20 photos des donn\u00e9es des pilotes. Cela veut dire que DriverDataLogs[3] repr\u00e9sente toutes les infos des pilotes dans le tour 4 et que DriverDataLogs[3][0] repr\u00e9sente toutes les infos du premier pilote dans le tour 3. Si on analyse un peu ce qui est \u00e9crit avec ces informations, on peut voir que je d\u00e9termine qu'un nouveau tour se d\u00e9finis comme une photo ou le troisi\u00e8me secteur a \u00e9t\u00e9 compl\u00eat\u00e9 et ou il ne l'\u00e9tait pas juste avant. Cela fait sens car quand un pilote compl\u00eate son troisi\u00e8me secteur c'est la que son dernier temps au tour se met \u00e0 jour. Le reste des tests est juste la pour \u00e9viter les faux positifs dans le cas ou un des deux DriverDataLogs[x][x] soit corrompu et que donc la valeur n'aie pas de sens. Cela veut dire que dans des conditions tr\u00e8s sp\u00e9cifiques je pourrais potentiellement rater un tour mais il faudrait vraiment que l'OCR me joue un vilain tour.","title":"Quand un pilote a fini un tour"},{"location":"index.html#quand-un-pilote-a-fait-un-arret-aux-stands","text":"Et la on touche le plus difficile. Pourtant un arr\u00eat aau stand ne devrait pas \u00eatre compliqu\u00e9 \u00e0 detecter. C'est quand un pilote change de pneu. Alors il peut changer de pneu en gardant le m\u00eame type de pneu et donc tout repose sur le nombre de tour qu'un pneu fait. Sauf que il faut ajouter \u00e0 cette reflexion qu'un pneu peut \u00eatre chauss\u00e9 sans qu'il soit neuf. Ce qui veut dire que l'on ne peut pas simplement choisir qu'un pilote a chang\u00e9 de pneus quand ses pneus sont \u00e0 1 tour. Il peut tr\u00e8s bien reprendre des pneus de qualif qui peuvent avoir 10 tours dans les pattes. En plus quand le pilote change de pneus il y a un phenomene assez p\u00e9nible qui fait que les deux premiers tours faits avec ne sont pass vraiment d\u00e9chiffrables car ils sont un peu cach\u00e9s derri\u00e8re la lettre qui indique le nouveau pneu chauss\u00e9. Exemple : \"Infographie de pneu qui se chevauche\" Voici le code que j'avais \u00e9crit pour tenter de trouver quand un pilote avait fait un pitstop : if ( data . CurrentTyre . Coumpound != Tyre . Type . Undefined && data . CurrentTyre . NumberOfLaps == 0 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. CurrentTyre . NumberOfLaps != 0 ) { //Do stuff } On peut voir que j'essaie de detecter quand le pneu est \u00e0 0 tours (ce qui est le moment ou il y a une lettre \u00e0 la place d'un num\u00e9ro de tour) et que la photo d'avant montrait un pneu normal. On v\u00e9rifie aussi que le pneu a bien \u00e9t\u00e9 detect\u00e9 en verifiant que le pneu n'est pas de type undefined. Le soucis c'est que ce n'est pas rare que l'OCR nous retourne qu'un pneu est vieux de 0 tours et donc il est absolument impossible de faire confiance \u00e0 cette metric. Si on veut utiliser cette methode pour trouver les Pitstop il va falloir avant tout am\u00e9liorer l'OCR sur ce point. Ce soucis mets en lumi\u00e8re un principe assez important de l'informatique \"Ggarbage in, Garbage out\". Si les donn\u00e9es que je recoit ne sont pas g\u00e9niales, le r\u00e9sultat ne sera pas g\u00e9nial non plus. Ce qui est frustran,t c'est que la detection des pneus n'est pas si mal mais entre les chiffres qui se chevauchent ce qui nous fait lire 0 alors que c'est juste un un '1' derri\u00e8re un 'H' ou le 1% du temps ou le programme se trompe, on ne peut pour l'instant tout simplement rien faire de mieux.","title":"Quand un pilote a fait un arr\u00eat aux stands"},{"location":"index.html#affichage-des-donnees","text":"Maintenant que l'on a stock\u00e9 toutes ces donn\u00e9es, il faut en faire quelque chose sinon ca ne sert a rien. Afficher les donn\u00e9es est techniquement la partie la plus simple du projet. Il faut prendre les donn\u00e9es qui nous int\u00e9ressent de la base de donn\u00e9es et des r\u00e9sultats de l'OCR et les afficher sur des composants Windows Forms. Il y a deux types d'affichages actuellements mis en place dans le projet sur les trois pr\u00e9vus initialement :","title":"Affichage des donn\u00e9es"},{"location":"index.html#affichage-direct","text":"L'affichage direct est simplement l'affichage du r\u00e9sultat de l'OCR. Par exemple le classement live ainsi que les \u00e9carts entre les pilotes sont affich\u00e9s directement depuis les r\u00e9sultats de l'OCR. Ce ne sont pas forc\u00e9ment des donn\u00e9es prises dans la base de donn\u00e9e. Voici un exemple : for ( int driverCount = 0 ; driverCount < liveData . Count ; driverCount ++) { DriverData driver = liveData [ driverCount ]; lblDriverName . Text = driver . Name ; lblDriverPosition . Text = driver . Position ; lblDriverLapTime . Text = Reader . ConvertMsToTime ( driver . LapTime ); } C'est l'affichage le plus simple et celui qui est le plus soumis \u00e0 des erreurs. En effet, si un pilote est mal detect\u00e9 on le verra directement mal s'afficher dans la Form principale. C'est \u00e9galement l'affichage le moins int\u00e9ressant car il ne cr\u00e9e aucune information, il ne fait que remontrer les infos que l'on peut d\u00e9ja voir dans la F1TV \"Exemple d'affichage live\"","title":"Affichage direct"},{"location":"index.html#affichage-calcule","text":"La encore il y a plusieurs types d'affichages :","title":"Affichage calcul\u00e9"},{"location":"index.html#affichage-hybride","text":"Pour ce qui est des affichages hybrides ce sont des affichages qui vont chercher des informations lives et des informations dans la base de donn\u00e9e. Ils ne font pas de calculs \u00e0 proprement parler mais ils affichent plus d'informations que ce que montre la F1TV. Cela veut dire qu'ils repr\u00e9sentent un d\u00e9but de plusvalue par rapport \u00e0 l'alternative qu'est la page DATA de la F1TV. Ils ne sont pas beaucoup plus durs \u00e0 impl\u00e9menter mais demandent de faire des requ\u00eates \u00e0 la base de donn\u00e9e. Comme exemple d'affichage hybride on a l'affichage des cinq derniers tours d'un pilote. C'est un affichage qui ne demande aucun calcul mais qui permet deja de se rendre compte de si le pilote est enn train de perdre ou gagner du temps ou si il est constant, ce qui n'est pas possible actuellement avec la F1TV \u00e0 moins d'avoir une excellente m\u00e9moire. \"Fen\u00eatre d'informations \u00e0 propos d'un pilote\" Voici un exemmple du type de code necessaire pour afficher ce genre de donn\u00e9es: public List <( int LapTime , int Lap )> GetDriverLaptimes ( string driverName , int numberOfLaptimes ) { int driverId = GetDriverID ( driverName ); List <( int LapTime , int Lap )> lapData = new List <( int LapTime , int Lap )>(); string selectQuery = \"Select LapTime,Lap from Stats WHERE DriverID = @driverID ORDER BY Lap DESC LIMIT @limit\" ; using ( var command = new SQLiteCommand ( selectQuery , Connection )) { command . Parameters . AddWithValue ( \"@driverID\" , driverId ); command . Parameters . AddWithValue ( \"@limit\" , numberOfLaptimes ); SQLiteDataReader reader = command . ExecuteReader (); while ( reader . Read ()) { int lapTime = reader . GetInt32 ( 0 ); int lap = reader . GetInt32 ( 1 ); lapData . Add (( lapTime , lap )); } } return lapData ; } public void Display (){ List <( int LapTime , int Lap )> lapsInfos = Storage . GetDriverLaptimes ( driverName , 5 ); int id = 0 ; foreach (( int LapTime , int Lap ) lapData in lapsInfos ){ Labels [ id ]. Text = LapTime ; id ++; } } Note: Le code montr\u00e9 ici n'est pas forc\u00e9ment le code utilis\u00e9 dans le projet. D'une certaine facon les fen\u00eatres de bataille et de d\u00e9passements sont aussi des hybrides. \"Exemple fen\u00eatre des batailles\" Ici ce sont les batailles qui sont repr\u00e9sent\u00e9es. Aucune donn\u00e9e n'est calcul\u00e9e, c'est litterallement directement les donn\u00e9es de la F1TV, mais la nuance est qu'on ne montre que les pilotes qui sont en train de se battre et que on leur assigne une couleur selon \u00e0 quel point ils sont proches. On a une plus-value sur la F1TV sans pour autant faire de monstres calculs. Pour info, les pilotes consid\u00e8r\u00e9s comme \u00eatant en train de se battre sont les pilotes \u00e0 moins de trois secondes les uns des autres et les couleurs sont suivantes : Vert : Dans la zone de DRS (Moins d'une seconde) Jaune : Plus d'une seconde Noir : Plus de deux secondes \"Exemple fen\u00eatre des d\u00e9passements\" La c'est l'historique des d\u00e9passements qui est affich\u00e9. On pourrait presque dire que c'est un affichage compl\u00eatement calcul\u00e9 car ce ne sont pas des informations disponibles directement sur la F1TV cependant je dirais que cela reste un hybride car il n'y a aucuns calculs. On regarde juste les diff\u00e9rences entre l'ancienne position d'un pilote et la nouvelle et on affiche les changements.","title":"Affichage Hybride"},{"location":"index.html#affichage-totalement-calcule","text":"L'affichage compl\u00eatement calcul\u00e9 est un type d'affichage qui ne montre aucune information trouv\u00e9e sur la page de la F1TV. C'est le premier affichage \u00e0 traiter l'information qu'il trouve et il retourne des informations nouvelles. La nuance avec les affichages pr\u00e9dictif est qu'il ne cr\u00e9e pas r\u00e9ellement de l'information, il la d\u00e9duit. Le but est de prendre un certain nombre d'informations trouv\u00e9es sur la page de la F1TV et de calculer des choses pour faire ressortir des tendances \u00e0 l'utilisateur. Cependant on reste sur des informations factuelles. Ce sont des infos d\u00e9duites que techniquement unn humain avec une bonne m\u00e9moire et fort en calcul mental pourrait faire. Mais la c'est fait automatiquement pour tous les pilotes et c'est affich\u00e9 de sorte \u00e0 faire ressortir les valeurs sp\u00e9ciales. Comme c'est un peu plus abstrait, je pense qu'un exemple vaut mieux que 1000 mots. \"Exemple de fen\u00eatre d'informations totalement calcul\u00e9es\" Ci dessus on peut voir un bon exemple. C'est une fen\u00eatre qui montre qui sont les pilotes les plus rapides et les moins rapides et qui montre la diff\u00e9rence de temps au tour. Cette information est totalement d\u00e9duite et n'est en aucun cas trouvable sur la F1TV mais elle n'est pas invent\u00e9e. Elle est simplement calcul\u00e9e. La formule est assez simple, je prend les cinq derniers temps au tour de tous les pilotes. Je fais une moyenne qui donne un temps. Et ensuite je trie les pilotes en fonction de ce temps et je n'affiche que les cinq plus rapide et les cinq plus lents. Ensuite il suffit de prendre le temps le plus rapide et faire une petite soustraction pour avoir l'\u00e9cart. C'est une stat assez int\u00e9ressante car elle lisse les diff\u00e9rences d'un tour \u00e0 l'autre et fait ressortir une tendance. On peut voir pour le grand prix de monaco 2023 par exemple, le moment ou les pneus pluie deviennent plus int\u00e9ressants que les pneus secs car on voit que les pilotes les plus rapides sont les pilotes de fond de grille qui ont chauss\u00e9 les pneus pluie en premier tandis que les plus lents sont les pilotes sur pneus secs vieux. Voici un bout de code qui s'occupe de faire les calculs : List <( int avg , string driverName )> averages = new List <( int avg , string driverName )>(); foreach ( DriverData driver in LiveDriverDataLogs [ LiveDriverDataLogs . Count - 1 ]) { //We want to recover the last 5 lap times List <( int lapTime , int lap )> laps = Storage . GetDriverLaptimes ( driver . Name , 5 ); if ( laps . Count > 0 ) { int avg = 0 ; foreach (( int lapTime , int lap ) lap in laps ) { avg += lap . lapTime ; } avg = avg / laps . Count ; averages . Add (( avg , driver . Name )); } }","title":"Affichage totalement calcul\u00e9"},{"location":"index.html#affichage-predictif","text":"C'est ici que ca devient vraiment dommage, le projet a mannqu\u00e9 de temps pour impl\u00e9menter des affichages pr\u00e9dictifs mais le potentiel est la ! Un affichage pr\u00e9dictif est un affichage qui cr\u00e9e des informations \u00e0 partir des infos qu'il a mais qui fait plus qu'un calcul. Le but est de tenter de deviner quelque chose. Voici des exemples d'affichages pr\u00e9dictifs qui pourraient \u00eatre mis en place averc l'architecture actuelle du projet : Si un pilote a des pneus depuis plus de 20 tours et que son temps au tour est en chute libre depuis cinq tours. Alors ce pilote va peut-\u00eatre devoir s'arr\u00eater. Si un pilote tourne une seconde au tour plus vite que le pilote devant lui et que ce pilote est \u00e0 10 secondes devant, alors il devrait pouvoir le rattraper d'ici dix tours. Si un arr\u00eat au stand est en moyenne de 23 secondes, alors un pilote 3\u00e8me ressortirais potentiellement 7\u00e8me si il s'arr\u00eate maintenant. Tous ces exemples sont des mini algorythmes pr\u00e9dictifs qui pourraient \u00eatre impl\u00e9ment\u00e9s assez facilement dans l'architecture actuelle du projet et pourraient apporter une immense plus-value si ils sont bien param\u00eatr\u00e9s. On peut m\u00eame imaginer que l'algorythme se corrige tout seul si il voit qu'il a eu tort pour que les course suivante il puisse mieux s'en sortir. Les possibilit\u00e9es sont infinies !","title":"Affichage pr\u00e9dictif"},{"location":"index.html#tests","text":"Alors la on arrive \u00e0 la GROSSE erreur de ce projet... Si je ne pouvais changer qu'une seule chose \u00e0 ma facon de faire le projet apr\u00e8s coup c'est ici que je ferai le changement. Les tests sont d'une importance absolument capitale mais si ils sont bien faits, c'est surtout un moyen ultra efficace de v\u00e9rifier que du nouveau code est performant et est beaucoup plus pratique \u00e0 utiliser. Je pense sans rire que j'aurais pu gagner plusieurs jours de travail si j'avais travaill\u00e9 diff\u00e9remment vis-a-vis des tests.","title":"Tests"},{"location":"index.html#comment-ca-cest-passe","text":"D\u00e8s la cr\u00e9ation du planning pr\u00e9visionnel j'ai fait une erreur capitale. J'ai mis les tests en fin de developpement des features... Et je ne leur ai laiss\u00e9 que tr\u00e8s peu de temps tout en les mettant au milieu du chemin critique ce qui les rends particuli\u00e8rement vuln\u00e9rables si une t\u00e2che du chemin critique est retard\u00e9e. En fait dans ce projet je voulais surtout \u00e9viter de faire comme certains projets que l'on a pu avoir pendant notre formation. C'est \u00e0 dire que je ne voulais surtout pas oublier la doc. Alors j'ai agenc\u00e9 le projet pour commencer par les fondations de la doc, puis en incluant les p\u00e9riodes de programmation et entre ces derni\u00e8re ajouter des Tests dans les trous. Le soucis c'est que du coup les projets \u00e9taient un peu le dernier truc dont je devais me soucier ce qui a \u00e9t\u00e9 une tr\u00e8s mauvaise id\u00e9e. Je me suis retrouv\u00e9 \u00e0 devoir mordre sur les jours de tests car les t\u00e2ches de programmation mettaient plus de temps que pr\u00e9vu (qui elles-m\u00eame auraient p\u00fb \u00eatre plus courte avec une bonne utilisation des tests) et je mme suis retrouv\u00e9 \u00e0 passer outre les tests pour avancer sur le reste du projet. J'ai donc du en panique \u00e0 la toute fin du projet construire quelques tests \"unitaires\" dont l'utilit\u00e9 est tr\u00e8s limit\u00e9e car tout le travail a d\u00e9ja \u00e9t\u00e9 fait et que vu la complexit\u00e9 qu'a pris le projet, faire de vrais tests unitaires est devenu un peu trop compliqu\u00e9 pour valoir le coup. Les seuls tests \"unitaires\" (Je l'\u00e9cris entre quotes car ce ne sont pas vraiment des tests unitaires mais plut\u00f4t des tests tout courts car ils ne sont pas sp\u00e9cifiques) qu'il y a dans le projet final sont des tests exclusivement tourn\u00e9s sur l'OCR. Ils sont d\u00e9ja vraiment pratiques car cela me permet de tester d'autres algorythmes d'OCR et voir si les r\u00e9sultats sont meilleurs ou non mais c'est juste un peu trop tard quoi... Les tests unitaires que j'ai impl\u00e9ment\u00e9s sont un peu tous pareils au niveau du fonctionnement : On choisit une image dans une liste d'images pr\u00e9par\u00e9es qui sont scens\u00e9e repr\u00e9senter le type de donn\u00e9es rencontr\u00e9es par l'application en temps normal On lis le nom de l'image que j'ai mis manuellement en indiquant ce qui \u00e9tait marqu\u00e9 sur l'image On fait un coup d'OCR sur l'image et on compare ce r\u00e9sultat avec la valeur que l'on est scens\u00e9 retrouver En pratique on est sur un code de ce style : [TestMethod()] public void SectorOCR_Test () { string directory = @\"./../../TestImages/Sectors/\" ; foreach ( string file in Directory . GetFiles ( directory )) { Bitmap image = ( Bitmap ) Image . FromFile ( file ); DriverSectorWindow sectorsWindow = new DriverSectorWindow ( image , new Rectangle ( 0 , 0 , image . Width , image . Height ), 1 , true ); string [] paths = file . Split ( '/' ); string fileName = paths [ paths . Length - 1 ]; fileName = fileName . Replace ( \".png\" , \"\" ); int timeMS = ( int ) sectorsWindow . DecodePng (); string time = Reader . ConvertMsToTime ( timeMS ); string [] checkDigits = fileName . Split ( '_' ); string [] digitsToCheck = time . Split ( ':' ); if ( time == \"0:00:000\" ) { Assert . AreEqual ( 0 , Convert . ToInt32 ( checkDigits [ 0 ])); } else { //The ConvertMSToTime will always return three chars so we need to make the checkDigits be also three chars while ( checkDigits . Length != 3 ) checkDigits = new [] { \"0\" }. Concat ( checkDigits ). ToArray (); for ( int i = 0 ; i < checkDigits . Length ; i ++) { //We need to convert to int first because sometimes we have \"08\" and \"8\" and in string its not the same but in int it is Assert . AreEqual ( Convert . ToInt32 ( checkDigits [ i ]), Convert . ToInt32 ( digitsToCheck [ i ])); } } } } Tout le code suppl\u00e9mentaire qui ne fait pas partie des \u00e9tapes cit\u00e9es est juste la pour manipuler le format des r\u00e9sultats pour qu'il soit comparable. \"Exemple d'\u00e9chantillons pour les tests\" Ce qui est pratique avec cette approche, c'est qu'il est tr\u00e8s facile de rajouter des cas sp\u00e9cifiques et voir comment le programme les g\u00e8re. Si je vois qu'un certain nombre est souvent mal reconnu, je peux faire expr\u00e8s de le mettre dans le dossier et modifier mon code d'OCR jusqu'\u00e0 ce que le test passe. Si j'avais eu plus de temps, j'aurais s\u00fbrement pu ajouter de vrais tests unitaires qui testent des fonctions tr\u00e8s pr\u00e9cises. Par exemple, v\u00e9rifier que les diff\u00e9rents Windows sont bien appel\u00e9es et que les zones se cr\u00e9ent correctement ou m\u00eame plus simplement que la lecture du JSON au d\u00e9marrage marche bien. Il faut savoir que m\u00eame si je n'ai pas eu l'occasion d'\u00e9crire beaucoup de tests sous forme de code. Toute la phase de d\u00e9veloppement de l'OCR, j'ai pass\u00e9 plus d'une heure par jour \u00e0 analyser les r\u00e9sultats. Je gardais toutes les images des WINDOWS et je notais dans le nom du fichier ce que l'algorithme trouvait et je passais en revue manuellement les centaines de r\u00e9sultats pour isoler ceux qui posaient un probl\u00e8me. C'est comme \u00e7a que je me suis rendu compte par exemple que, avec cette police, les quatre et les 1 \u00e9taient souvent confondus. Donc m\u00eame si les tests automatis\u00e9s sont clairement insuffisants par rapport \u00e0 ce que j'aurais peut-\u00eatre d\u00fb faire, j'ai pass\u00e9 \u00e9norm\u00e9ment de temps \u00e0 tester mon application.","title":"Comment ca c'est pass\u00e9"},{"location":"index.html#comment-caurait-du-se-passer","text":"Si je devais refaire ce projet aujourd'hui, je pense que j'utiliserais un peu la m\u00eame technique que pour la doc. J'aurais mis les t\u00e2ches de Tests directement au d\u00e9but du projet et j'aurais d\u00e9termin\u00e9 le squelette de l'application par la m\u00eame occasion. Je pense que j'aurais mis trois jours pour \u00e9crire tous les tests dont j'aurais besoin et j'aurais fait une strat\u00e9gie de TDD (Test Driven Developpement) par ce que je pense que \u00e7a marcherait vraiment super bien sur ce type de projet. J'aurais pris, je pense, cinq une dizaine d'images compl\u00e8tes de la F1TV de plusieurs GP diff\u00e9rents et j'aurais mis toutes les fen\u00eatres d\u00e9coup\u00e9es dans des fichiers avec des tests comme ceux que j'ai faits pour ce projet. Et comme \u00e7a je saurai que mon algo est bon uniquement quand il aura r\u00e9ussi \u00e0 passer tous les tests. Cela r\u00e8glerait le souci que j'ai eu le plus : Me retrouver \u00e0 devoir changer l'OCR 5 fois par ce qu'\u00e0 chaque fois que je d\u00e9veloppe une nouvelle feature, je me rends compte d'une faiblesse, mon algorithme\u2026 Non seulement j'aurais eu beaucoup plus de facilit\u00e9 \u00e0 avancer sur le projet, mais en plus, je pense que cela m'aurait fait gagner \u00e9norm\u00e9ment de temps non seulement, car je n'ai plus \u00e0 tester tout \u00e0 la main, mais en plus par ce que \u00e7a veut dire que quand l'OCR passe les tests, je n'ai plus jamais \u00e0 m'en soucier.","title":"Comment \u00e7'aurait d\u00fb se passer"},{"location":"index.html#lecons","text":"Je pense que dans mes futurs projets, je mettrai les tests en d\u00e9but de projet plut\u00f4t qu'\u00e0 la fin et je ferai en sorte qu'ils fassent partie du chemin critique et que je ne puisse pas passer \u00e0 c\u00f4t\u00e9 sous pr\u00e9texte que \"Je n'ai pas le temps\". \u00c9crire des tests, ce n'est jamais marrant et c'est encore moins marrant quand ils nous emp\u00eachent d'avancer. Mais je suis convaincu qu'\u00e0 la fin, c'est un gain de temps et de s\u00e9r\u00e9nit\u00e9 incontournable.","title":"Le\u00e7ons"},{"location":"index.html#resume-des-difficultes-techniques","text":"Ici, je vais parler tr\u00e8s rapidement des difficult\u00e9s techniques rencontr\u00e9es. Si vous voulez tout savoir \u00e0 propos des difficult\u00e9s, vous pouvez aller lire le journal de bord. C'est aussi pour \u00e9viter de me r\u00e9p\u00e9ter par rapport aux explications des diff\u00e9rents points dans l'analyse organique. Je ne vais pas non plus parler des difficult\u00e9s rencontr\u00e9 avec des choses que je n'ai pas gard\u00e9es dans le programme final donc il est normal que vous vous disiez qu'il n'y a pas eu tant de difficult\u00e9s que \u00e7a.","title":"R\u00e9sum\u00e9 des difficult\u00e9s techniques"},{"location":"index.html#browser-headless","text":"Il y avait plusieurs difficult\u00e9s techniques avec cette histoire de Browser Headless. D\u00e9j\u00e0 pouvoir lancer un browser headless et le contr\u00f4ler. C'est difficile, car il faut trouver la bonne librairie et ensuite, il faut trouver le bon ex\u00e9cutable de gecko Driver qui permette de faire fonctionner l'application m\u00eame si l'utilisateur n'a pas Firefox sur sa machine. Ensuite, la seconde difficult\u00e9 est celle de ne pas se faire chopper comme un bot par le site de la F1TV. Il faut savoir qu'\u00e0 ce jour, je n'ai toujours pas r\u00e9ussi \u00e0 faire croire \u00e0 la page de login de la F1TV que j'\u00e9tais un user normal en utilisant S\u00e9l\u00e9nium mais au moins maintenant, je peux acc\u00e9der aux vid\u00e9os tranquillement. Ce souci de ne pas pouvoir se connecter avec la page de login \u00e0 la plus grosse difficult\u00e9 technique de cette partie du projet : la connexion automatique. Pour me connecter \u00e0 la F1TV avec un browser headless la seule solution que j'ai trouv\u00e9e a \u00e9t\u00e9 d'utiliser des cookies. Et pour que l'utilisateur n'ait pas \u00e0 aller chercher les siens dans son navigateur, il a fallu trouver une technique pour aller les chercher directement sans lui demander son avis. Autre difficult\u00e9, comme on travaille avec un site web que l'on ne contr\u00f4le pas, il faut trouver un moyen de g\u00e9rer les erreurs et de r\u00e9essayer parfois et attendre quand il faut dans les cas o\u00f9 le chargement est long etc... Ensuite, apr\u00e8s tout \u00e7a, la derni\u00e8re difficult\u00e9 a \u00e9t\u00e9 de pouvoir contr\u00f4ler le Firefox Headless assez bien pour qu'il puisse non seulement naviguer les pages, mais aussi qu'il puisse cliquer sur des boutons qui ne s'affichent pas tout le temps. (Je ne vais pas mentionner la difficult\u00e9 que c'\u00e9tait de mettre le browser en 4K pour des raisons de sant\u00e9 mentale)","title":"Browser Headless"},{"location":"index.html#ocr_2","text":"Les difficult\u00e9s ici sont dans un autre niveau. Chaque type de donn\u00e9e repr\u00e9sentait sa difficult\u00e9 \u00e0 lui tout seul, sans compter l'optimisation. Pour commencer, on a le texte pour les noms de pilotes. Il a fallu trouver un syst\u00e8me qui puisse reconnaitre le texte et qui puisse comparer le r\u00e9sultat avec les pilotes que l'on connait. Ensuite, il a fallu trouver un moyen de d\u00e9tecter la diff\u00e9rence entre les fen\u00eatres de DRS o\u00f9 il est ouvert ou ferm\u00e9. Il fallait \u00e9galement faire attention \u00e0 ne pas faire de faux positifs. Pour les temps par secteurs, il a fallu trouver des filtres qui permettent de bien diff\u00e9rencier les '1' et les '4' sans les confondre et il a aussi fallu trouver un moyen de filtrer l'image pour que dans le cas o\u00f9 le texte serait en couleur \u00e7a fonctionne quand m\u00eame. (Car oui, un filtre de nuances de gris ne marche pas super avec des couleurs sombres) Pour les pneus (le plus dur) il a fallu trouver un moyen de trouver sur toute la longueur de la zone la partie int\u00e9ressante. Ensuite, il a fallu trouver une technique pour savoir quel type de pneu c'est en fonction de la couleur moyenne et ensuite le plus dur a \u00e9t\u00e9 d'isoler le chiffre du dessin autour, car Tesseract n'aime pas les formes, le tout automatiquement. Pour les temps au tour, il a fallu trouver un moyen de ne pas confondre les ponctuations avec des chiffres tout en ne les ratant pas. Et il a fallu trouver un moyen de d\u00e9tecter quand in\u00e9vitablement cela arrive quand m\u00eame. Et la derni\u00e8re difficult\u00e9 (la plus p\u00e9nible) a \u00e9t\u00e9 de d\u00e9tecter les \u00e9carts entre les pilotes. Il a fallu trouver une fa\u00e7on de d\u00e9coder le texte en temps, mais aussi de faire tout un syst\u00e8me qui d\u00e9tecte et r\u00e8gle les cas ou un ':' a \u00e9t\u00e9 oubli\u00e9 ou confondu tout en ne sachant pas s'il \u00e9tait cens\u00e9 y en avoir \u00e0 la base, car les valeurs peuvent varier entre '1_23.657' et '0.452'.","title":"OCR"},{"location":"index.html#stockage_1","text":"Pour ce qui est du stockage, la grande difficult\u00e9 a \u00e9t\u00e9 de savoir quand un pilote avait fini un tour parce que chaque pilote finit son tour \u00e0 un moment diff\u00e9rent. Il a \u00e9galement fallu trouver un moyen de savoir les donn\u00e9es d'un pilote \u00e9taient logiques. Une difficult\u00e9 qui n'a pas \u00e9t\u00e9 compl\u00e8tement d\u00e9pass\u00e9e est de savoir quand un pilote a fait un arr\u00eat aux stands, car la d\u00e9tection de l'\u00e2ge des pneus est plus que mauvaise. Voil\u00e0. Ce fut une petite liste non exhaustive de quelques difficult\u00e9s techniques que j'ai rencontr\u00e9es pendant ce projet.","title":"Stockage"},{"location":"index.html#optimisation-du-programme","text":"Ici, je vais parler des techniques que j'ai utilis\u00e9es pour r\u00e9duire le temps de traitement de chaque image de 50 secondes \u00e0 un peu moins de 3 sur le processeur de mon laptop. En effet, dans les premi\u00e8res versions du projet, traiter l'int\u00e9gralit\u00e9 d'une image pouvait prendre presque une minute. Ce qui est compliqu\u00e9 dans ce projet, c'est qu'il y a un certain nombre de choses que je ne contr\u00f4le pas. En utilisant Tesseract, je me retrouve avec des incompressibles. En imaginant que l'OCR sur une image prenne 300 ms, m\u00eame si j'avais 180 threads capables de faire cette t\u00e2che en m\u00eame temps, le temps de traitement sera toujours d'au moins 300 ms. Cr\u00e9er une instance de Tesseract prend \u00e9galement du temps. Ma mission n'est donc pas d'arriver \u00e0 des temps de quelques dizaines de millisecondes, mais plut\u00f4t de rajouter le moins de temps possible pendant le traitement et de tenter de faire le plus de choses possible en parall\u00e8le. Voici la liste des choses qui prennent du temps : Lancement du navigateur et navigation Cr\u00e9ation des instances de Tesseract Filtrage des images OCR Ce sont les quatre gros postes qui coutent le plus cher en ressources. Mais par chance, deux de ces postes ne sont appel\u00e9s qu'une seule fois au d\u00e9marrage, ce qui fait que ce n'est pas catastrophique s'ils prennent du temps. Tandis que l'OCR et le filtrage est fait \u00e0 chaque d\u00e9tection. Pour ce qui est du d\u00e9marrage, malheureusement, on ne peut pas faire grand-chose. Lancer le browser et naviguer \u00e0 travers la F1TV prend du temps, surtout si la connexion du client est mauvaise. Pour certaines actions, j'ai fait un syst\u00e8me qui essaie pendant 10 secondes de cliquer sur un bouton plut\u00f4t que d'attendre 10 secondes et cliquer pour tenter d'\u00e9conomiser un peu, mais malheureusement, c'est lent et on ne peut pas y faire grand-chose. Pour la g\u00e9n\u00e9ration des instances de Tesseract, c'est un peu pareil, mais pour d'autres raisons. Comme Tesseract n'est pas \"Thread Safe\" (Ce qui veut dire qu'il n'est pas parall\u00e9lisable), si on veut faire plusieurs reconnaissances \u00e0 la fois, il faut plusieurs instances de Tesseract load\u00e9es en m\u00e9moire. J'ai donc d\u00e9cid\u00e9, pour une question de simplicit\u00e9 et de performances, de faire en sorte que chaque fen\u00eatre de donn\u00e9e ou \"Window\" aie sa propre instance de Tesseract. Vous qui lisez ces lignes \u00eates peut-\u00eatre en train de vous dire \"Oulala mais \u00e7a doit beaucoup de m\u00e9moire son truc l\u00e0 \" et vous auriez parfaitement raison ! \"Consommation de m\u00e9moire peu apr\u00e8s avoir commenc\u00e9 la d\u00e9tection\" Ce programme consomme en effet une quantit\u00e9 absolument catastrophique de m\u00e9moire vive. Mais si je l'ai fait, c'est pour une bonne raison. Cela prend juste beaucoup trop de temps de cr\u00e9er une nouvelle instance \u00e0 chaque boucle de Tesseract et c'est encore plus long de faire toutes les op\u00e9rations d'OCR les unes apr\u00e8s les autres pour n'avoir qu'un seul Tesseract de load\u00e9. On peut parfois arriver \u00e0 des chiffres qui approchent les 4GB de RAM ce qui est absolument RIDICULE. Cependant, c'est un compromis que j'\u00e9tais pr\u00eat \u00e0 faire pour avoir une application qui soit plus rapide. Je suis absolument certain que cette solution et les autres solutions que j'ai trouv\u00e9es pour ce projet ne sont pas les meilleures ou les plus efficaces. Mais ce sont les solutions que j'ai trouv\u00e9 pour faire en sorte que le projet avance et fonctionne \u00e0 peu pr\u00e8s vite. Ensuite pour ce qui est de ce qui se passe \u00e0 chaque boucle, l\u00e0 le mot magique, c'est \"Parall\u00e8le\". Le traitement de toutes les zones est fait en m\u00eame temps. La structure du projet en zones, sous zones et fen\u00eatres de donn\u00e9es fait qu'il est assez facile de venir parall\u00e9liser le processus si on les impl\u00e9mente correctement. Diagramme qui montre comment les zones et fen\u00eatres interagissent On peut voir sur ce diagramme que la zone principale demande \u00e0 toutes les sous zones de d\u00e9coder leur contenu. Ces derni\u00e8res font l'exacte m\u00eame chose avec les fen\u00eatres de donn\u00e9es qui retournent chacune ce qu'elles contiennent apr\u00e8s un coup d'OCR et ensuite les zones recombinent les informations et les envoient \u00e0 la zone principale. Tout cela est tr\u00e8s bien, mais quel rapport avec la parall\u00e9lisation ? Eh bien, comme chaque zone de pilote est ind\u00e9pendante, on peut tout simplement faire une boucle for parall\u00e8le qui appelle toutes les zones pilotes. On passe de 15 \u00e0 20 secondes de traitement \u00e0 un peu plus de trois juste avec cette technique. Alors \u00e7a n'\u00e9tait pas simple \u00e0 impl\u00e9menter, car il a fallu programmer les zones de sorte qu'elles soient toutes ind\u00e9pendantes les unes des autres. Mais une fois que le travail en amont a \u00e9t\u00e9 effectu\u00e9, il est tr\u00e8s simple de parall\u00e9liser. Les filtres fonctionnent de la m\u00eame fa\u00e7on sauf que l\u00e0, on parall\u00e9lise le traitement de chaque ligne dans une image. L'impact est moindre qu'avec les zones, mais si on teste avec une machine assez puissante cela pourrait faire la diff\u00e9rence. Seul souci avec cette m\u00e9thode, cela veut dire que le processeur est particuli\u00e8rement sollicit\u00e9 '^^... \"Utilisation du processeur pendant le fonctionnement de l'application\" Mon laptop ne poss\u00e8de malheureusement que six coeurs ce qui limite pas mal la puissance de la paralellisation. Mais je suis convaincu qu'avec un CPU avec plus de coeurs on pourrait arriver \u00e0 d'encore meilleurs r\u00e9sultats. Mais cette utilisation du processeur a aussi un inconv\u00e9nient... \"Temp\u00e9ratures du laptop pendant le fonctionnement de l'application\" ; Donc si je veux commenter la F1 avec cet outil, note \u00e0 moi m\u00eame, je ne dois pas utiliser le laptop si je ne veux pas me cramer les doigts. Si je pouvais utiliser le GPU pour acc\u00e8l\u00e9rer le processus on pourrait peut-\u00eatre avoir de meilleurs r\u00e9sultats mais de ce que j'ai pu lire, l'OCR n'est pas sp\u00e9cialement un bon use case pour les GPU. Pour conclure, je dirais que ce projet est loin d'\u00eatre un exemple de performances et clairement, il y a des choix discutables qui ont \u00e9t\u00e9 faits et d'une mani\u00e8re g\u00e9n\u00e9rale, si je devais refaire tout le projet avec la performance en premier objectif, j'aurais s\u00fbrement fait diff\u00e9remment. Maintenant, avec le temps que j'ai eu, je suis d\u00e9j\u00e0 content d'avoir pu faire quelque chose qui fonctionne et qui ne prenne pas une minute \u00e0 traiter une image.","title":"Optimisation du programme"},{"location":"index.html#ethique-du-projet","text":"Ici, on va parler des questions \u00e9thiques de ce projet. En effet, il y a quelques petites choses qui peuvent soulever une question. Il y a deux questions qui reviennent presque \u00e0 chaque fois que je parle ou pr\u00e9sente mon projet :","title":"Ethique du projet"},{"location":"index.html#utilisation-abusive-de-la-f1tv","text":"La F1TV est un service payant qui n'est pas forc\u00e9ment donn\u00e9 (m\u00eame si pas bien cher pour un utilisateur comme moi qui l'utilise plus d'une fois par semaine plusieurs heures). De ce fait, je ne peux pas rendre son acc\u00e8s plus facile ou faire fuiter des informations de courses que l'on ne peut se procurer que par son utilisation. Mais voil\u00e0 pourquoi je pense que mon utilisation n'est pas une utilisation abusive : L'application ne fonctionne que si l'utilisateur a un compte F1TV valide et qu'il s'est connect\u00e9 r\u00e9cemment sur sa machine. (Cela veut donc dire que je ne permets pas \u00e0 des utilisateurs de frauder) L'application ne partage aucune information sur le contenu de la F1TV avec l'ext\u00e9rieur. (On ne peut pas avoir acc\u00e8s \u00e0 des informations payantes sans abonnement) L'application ne simule qu'un seul utilisateur connect\u00e9 sur une vraie machine (Cela veut donc dire que je ne suis pas en train de faire un syst\u00e8me de bot qui regarde 45 flux en m\u00eame temps pour scrapper tout le site et/ou poser des probl\u00e8mes de DDOS) Les donn\u00e9es ne sont pas stock\u00e9es entre les sessions (cela veut dire que l'on ne repr\u00e9sente pas un risque de fuite de donn\u00e9es et on n'est pas un service qui vient scrapper le contenu pour alimenter une IA ou quoi que ce soit... pour l'instant...) En fait mon application fonctionne exactement comme si on avait une page ouverte avec la F1TV dessus et qu'un ami \u00e0 c\u00f4t\u00e9 de nous la regardait en prenant des notes pour nous aider \u00e0 suivre. Je ne vois donc pas le mal et je ne vois pas en quoi ce projet serait probl\u00e9matique sur ce point. Apr\u00e8s dans le futur, le but est clairement de conserver les infos trouv\u00e9es pour entrainer un algorithme de pr\u00e9diction et l\u00e0 peut-\u00eatre que cela pourrait poser plus de probl\u00e8mes, mais ce n'est pas le cas \u00e0 l'heure ou j'\u00e9cris ces lignes.","title":"Utilisation abusive de la F1TV ?"},{"location":"index.html#recuperation-de-cookies-a-linsu-de-lutilisateur","text":"Alors l\u00e0, on est clairement sur le sujet un peu plus \u00e9pineux... Un peu de contexte d'abord : \u00c0 la base, je voulais que l'utilisateur entre ses identifiants dans mon application et ensuite que le navigateur les rentre dans la page de login automatiquement et qu'il puisse se connecter. Deux probl\u00e8mes \u00e0 cette solution : L'utilisateur doit avoir assez confiance en mon programme pour laisser ses identifiants en clair \u00e0 l'int\u00e9rieur. Il est extr\u00eamement difficile de bypass la protection contre les bots de la page de login de la F1TV. J'ai donc d\u00fb trouver une autre solution : Utiliser les cookies ! Le seul souci, c'est que cela voulait dire que l'utilisateur devait aller chercher lui-m\u00eame ses cookies dans le navigateur en utilisant F12 et qu'il devait \u00e0 nouveau me faire confiance pour que je n'en fasse rien. Je trouvais cette solution trop p\u00e9nible pour l'utilisateur alors, j'ai d\u00e9cid\u00e9 d'en trouver une autre. Utiliser les cookies MAIS, sans demander \u00e0 l'utilisateur. Pour faire simple, mon programme va directement d\u00e9coder les cookies encrypt\u00e9s dans la base de donn\u00e9e SQLITE de Chrome, va les stocker dans un CSV en clair et va laisser mon programme C# aller piocher ceux qui l'int\u00e9ressent. Soucis, mon programme a acc\u00e8s \u00e0 tous les cookies de l'utilisateur \u00e0 son insu, cela veut dire que je pourrais les utiliser \u00e0 des fins peu scrupuleuses. Et c'est la solution que j'ai d\u00e9cid\u00e9 de choisir, car elle permet \u00e0 l'utilisateur de ne rien avoir \u00e0 faire pour se connecter depuis l'application, mais cela veut aussi dire qu'il doit me faire confiance pour ne pas utiliser tous ces cookies pour mon utilisation personnelle. Sauf que contrairement aux autres solutions, il ne sait pas qu'il est en train de me faire confiance donc \u00e7a va. :D Non plus s\u00e9rieusement, oui, je pourrais faire n'importe quoi avec les cookies de l'utilisateur, non, je ne vais pas le faire, et non, je ne pr\u00e9vois jamais de le faire. Mais il est int\u00e9ressant de mentionner que mon application met en p\u00e9ril la s\u00e9curit\u00e9 des cookies de l'utilisateur et qu'il serait bien dans le futur de mettre un message explicatif au premier d\u00e9marrage ou dans l'installeur de l'application pour pr\u00e9venir l'utilisateur.","title":"R\u00e9cup\u00e9ration de cookies \u00e0 l'insu de l'utilisateur ?"},{"location":"index.html#utilisation-de-chat-gpt","text":"\"Logo chat Gpt\" Cette ann\u00e9e, ChatGPT est venu s'installer dans la liste des outils que j'utilise presque quotidiennement pour avancer sur mes projets. J'ai utilis\u00e9 ChatGPT un certain nombre de fois pendant ce travail et je pense qu'il m'a fait gagner un certain nombre d'heures. En effet, dans certains cas tr\u00e8s pr\u00e9cis, ChatGPT est une ressource absolument g\u00e9niale. Je l'ai surtout utilis\u00e9 quand j'avais de soucis avec des librairies ou pour faire du troubleshooting. Ce que j'aime beaucoup avec ChatGPT c'est qu'il s'adapte \u00e0 ce qu'on lui donne. Par exemple, il m'est souvent arriv\u00e9 de vouloir utiliser des librairies comme Puppeteer sharp ou des exemples sont difficilement trouvables sur internet normalement. Et quand je voulais simplement faire fonctionner un exemple tr\u00e8s rapide, il a quasiment toujours pu me fournir le code minimum. Cependant, d\u00e8s que l'on arrive sur des cas encore plus pr\u00e9cis, on atteint assez vite les limites du syst\u00e8me. J'ai fr\u00e9quemment fait appel \u00e0 cet outil pour diagnostiquer du code, que ce soit pour d\u00e9tecter un souci ou m\u00eame plus juste pour voir si mon code avait du sens. En effet, si on donne une m\u00e9thode \u00e0 chatGPT, il va tenter de l'expliquer, et s'il n'y arrive pas, c'est g\u00e9n\u00e9ralement que les variables sont mal nomm\u00e9es ou qu'il y a un souci avec la logique du code. Et pour ce qui est de la d\u00e9tection des erreurs, l'exemple que je peux donner c'est quand je faisais des m\u00e9thodes asynchrones et parall\u00e8les, je pouvais lui donner la m\u00e9thode avec l'erreur que je ne comprends pas et il peut me donner cinq raisons de possibles soucis. Cependant, je pensais utiliser beaucoup plus ChatGpt mais \u00e0 la fin l'outil est assez limit\u00e9 et je ne l'utilisais que quand mes recherches internet \u00e9taient infructueuses. Le seul cas o\u00f9 il m'a un peu sauv\u00e9, c'est quand je travaillais avec Puppeteer et que j'essayais de r\u00e9gler un souci qui faisait que le programme plantait \u00e0 chaque fois que j'ouvrais une vid\u00e9o. Au bout de quelques heures de gal\u00e8re, il m'a juste propos\u00e9 d'utiliser une autre librairie comme s\u00e9l\u00e9nium et il m'a converti tout mon code puppeteer en code utilisable par Selenium, et m\u00eame si cela a demand\u00e9 un peu plus de travail que de copier-coller, pour finir, j'ai pu avoir quelque chose qui marchait et je n'aurais peut-\u00eatre pas eu le r\u00e9flexe ou l'envie de le faire si je n'avais pas utilis\u00e9 cet outil. En conclusion, certaines m\u00e9thodes de mon projet ont \u00e9t\u00e9 faites avec l'aide de ChatGPT mais c'est une minorit\u00e9 et je l'ai surtout utilis\u00e9 pour comprendre des erreurs et pour avoir des pistes \u00e0 explorer pour les fix. Rien de bien fou.","title":"Utilisation de Chat GPT"},{"location":"index.html#ameliorations-futures","text":"Ici, je vais parler de deux types d'am\u00e9liorations. Les am\u00e9liorations \u00e0 court terme, que j'aurais pu faire si je n'avais pas perdu autant de temps sur certains probl\u00e8mes techniques ou si j'avais eu quelques semaines de plus pour travailler sur le projet. Et les id\u00e9es qui seraient plus compliqu\u00e9es \u00e0 mettre en place que je n'aurais jamais pu ajouter \u00e0 ce travail dans le temps imparti, mais qui sont maintenant possibles si je continue pendant quelques mois \u00e0 travailler sur le projet.","title":"Am\u00e9liorations futures"},{"location":"index.html#court-terme","text":"Je vais commencer par les petites am\u00e9liorations. Chose que je regrette le plus, je dirai, c'est tout ce qui est affichage. J'aurais vraiment aim\u00e9 faire une magnifique interface, mais il m'a manqu\u00e9 de temps pour en faire une plus jolie et plus facile d'utilisation. Une am\u00e9lioration r\u00e9ellement n\u00e9cessaire serait d'am\u00e9liorer la d\u00e9tection des pneus pour qu'il soit possible de correctement d\u00e9tecter les arr\u00eats aux stands. En g\u00e9n\u00e9ral, si j'avais pu mettre plus de temps dans l'analyse des donn\u00e9es que je re\u00e7ois de la F1TV, j'aurais pu faire un syst\u00e8me plus efficace de d\u00e9tection de d\u00e9passements, car la version actuelle n'est vraiment pas bonne. Trouver un moyen de faire des erreurs plus pr\u00e9cises. En effet, maintenant, certaines erreurs ont des causes qui peuvent \u00eatre multiples (qui peuvent \u00eatre caus\u00e9es par un mauvais lien, ou une erreur de r\u00e9cup\u00e9ration des cookies ou m\u00eame juste de connexion internet). \u00c7a demanderait simplement un peu plus de temps pour qu'au lieu de retourner seulement une erreur, on tente de r\u00e9cup\u00e9rer plus d'infos pour la rendre plus sp\u00e9cifique. Et pour les am\u00e9liorations un peu plus concr\u00e8tes : Impl\u00e9menter plus d'affichages calcul\u00e9s. J'aurais aim\u00e9 ajouter des affichages comme le classement pond\u00e9r\u00e9 des pilotes en fonction des arr\u00eats aux stands. Cela demanderait juste un peu de temps et d'am\u00e9liorer la d\u00e9tection des pitstops. Impl\u00e9menter des affichages pr\u00e9dictifs simples. On pourrait imaginer des algorithmes simple qui pourraient tenter de pr\u00e9dire quand un pilote va en rattraper un autre ou quand un pilote va devoir s'arr\u00eater en fonction des temps aux tours. \u00c7a ne me demanderait pas de nouvelles technologies, mais simplement du temps pour mettre en place et tester les algorithmes. Faire un syst\u00e8me qui puisse tester les algorithmes pr\u00e9dictifs sur un panel de Grand Prix. Si l'\u00e9tape d'avant est faite, on peut facilement imaginer un bout de programme qui aille tester le programme sur diff\u00e9rents Grand Prix pour voir si les pr\u00e9dictions sont bonnes. Avoir une notion d'historique des courses pour avoir une page de comparaison des performances des \u00e9quipes. Par exemple, d\u00e9terminer quelle voiture est la plus rapide et comparer avec les autres circuits. On peut m\u00eame imaginer qu'apr\u00e8s plusieurs Grands Prix, on puisse tenter de d\u00e9terminer quelle \u00e9quipe est forte sur quel circuit. Avoir un syst\u00e8me qui permet de trouver automatiquement tous les liens de Grand Prix comme \u00e7a l'utilisateur n'aie plus besoin d'aller chercher un URL. Faire un installer pour qu'un utilisateur n'ait pas \u00e0 se taper la proc\u00e9dure d'installation (qui est assez p\u00e9nible) \u00e0 la main.","title":"Court terme"},{"location":"index.html#long-terme","text":"L\u00e0, on va se pencher sur des features qui prendraient plus d'un mois \u00e0 mettre en place correctement. On pourrait imaginer un syst\u00e8me qui puisse regarder 50 Grand Prix et qui change automatiquement les variables des algorithmes en fonction de leurs performances (Un genre d'apprentissage machine rustique). On pourrait imaginer un syst\u00e8me qui puisse cr\u00e9er des infographies. Que ce soit au milieu de la course ou \u00e0 la fin, le programme pourrait nous g\u00e9n\u00e9rer des images avec une stat int\u00e9ressante (ex : x pilote a fait x d\u00e9passements ou x pilote gagnerait x points s'il finissait dans cette position, ce qui le ferait changer de position au classement g\u00e9n\u00e9ral). Si c'est bien fait, cela pourrait \u00eatre un outil extr\u00eamement pr\u00e9cieux, car je pourrais utiliser ces infographies dans mes commentaires. On pourrait avoir un syst\u00e8me qui donne une note de performance pour chaque pilote en fonction de ses performances et en fonction de sa voiture pour faire un genre de classement des pilotes. Il serait g\u00e9nial d'avoir une page de stats qui se souviennent de tous les anciens Grand Prix regard\u00e9s qui permettent d'afficher toutes les stats d'un pilote sur plusieurs courses. (Cela me permettrait, dans des moments o\u00f9 la course stagne un peu, de pouvoir prendre n'importe quel pilote et d'avoir des choses \u00e0 dire \u00e0 son sujet) On pourrait m\u00eame imaginer un syst\u00e8me qui utilise une base de donn\u00e9es sur un serveur Infomaniak et d\u00e9velopper une extension de navigateur qui me donne des infos importantes directement sur la page o\u00f9 je commente le Grand Prix ou qui au moins me fasse des notifications pour que je sache quand aller regarder, car un truc important s'est pass\u00e9. Je vais m'arr\u00eater l\u00e0 parce que les possibilit\u00e9s sont tout simplement infinies. \u00c0 partir du moment o\u00f9 je peux r\u00e9cup\u00e9rer toutes les informations de la F1TV de mani\u00e8re fiable, les champs des possibles sont ouverts et la seule limite est notre imagination. Il n'est pas impossible que je refasse une version de ce projet dans le futur qui me permette d'appliquer tout ce que j'ai appris pour le faire plus proprement (avec de la TDD par exemple) et qui me permette d'impl\u00e9menter toutes ces am\u00e9liorations et plus. Je pense vraiment que si je continue \u00e0 commenter pour le 20 minutes dans les ann\u00e9es qui viennent, cela pourrait \u00eatre int\u00e9ressant de d\u00e9velopper un outil du style qui pourrait grandement m'aider \u00e0 faire des commentaires de qualit\u00e9.","title":"Long terme"},{"location":"index.html#conclusion","text":"","title":"Conclusion"},{"location":"index.html#bilan","text":"Je vais faire un petit bilan de ce travail. D\u00e9j\u00e0, je vous remercie chaleureusement d'avoir lu cette documentation (j'ai d\u00fb la relire en entier une ou deux fois, je sais que ce n'est pas facile). J'esp\u00e8re que j'ai pu parler de tout ce dont je voulais parler et que je l'ai fait de mani\u00e8re explicite et ais\u00e9 \u00e0 lire pour vous. J'ai r\u00e9ellement fait de mon mieux pour qu'elle soit la plus simple possible \u00e0 lire, mais c'est un exercice difficile dans un document de cette taille et je m'excuse des in\u00e9vitables erreurs et coquilles que vous aurez peut-\u00eatre remarqu\u00e9. Je dois avouer que je suis quand m\u00eame tr\u00e8s content d'arriver au bout de ce travail. J'ai vraiment aim\u00e9 cette exp\u00e9rience unique de pouvoir travailler \u00e0 100\u2009% sur un projet et voir de quoi je suis capable. Mais je suis aussi heureux d'arriver \u00e0 la fin, car je dois avouer que \u00e7a n'a pas \u00e9t\u00e9 simple tous les jours et que de travailler presque seul sur un projet si long n'est pas facile. Pour \u00eatre tout \u00e0 fait honn\u00eate, je suis quand m\u00eame fier de ce que j'ai fait (ce qui n'arrive pas souvent). C'est un projet qui est \u00e0 des ann\u00e9es lumi\u00e8res de la perfection, mais c'\u00e9tait mon id\u00e9e et en commen\u00e7ant le projet, je ne savais m\u00eame pas si j'allais y arriver. Certes le r\u00e9sultat n'est pas exactement comme je l'aurais r\u00eav\u00e9, mais il est concret et il fonctionne ! Il y a eu des moments ou en voyant la quantit\u00e9 de choses qu'il restait \u00e0 faire, je me sentais un peu d\u00e9courag\u00e9, mais je suis arriv\u00e9 au bout avec un projet fonctionnel et pour \u00e7a, je suis assez fier. Ce fut un projet difficile, surtout sur le plan de la r\u00e9solution de probl\u00e8mes. Chaque \u00e9tape du projet apportait une nouvelle probl\u00e9matique qu'il fallait r\u00e9soudre et si parfois, j'ai pu trouver des fa\u00e7ons \u00e9l\u00e9gantes de le faire, pour d'autres, il a fallu \u00eatre un peu plus cr\u00e9atif et moins regardant sur la m\u00e9thode, mais que sur le r\u00e9sultat. Je suis un peu frustr\u00e9 de rendre le projet alors que j'ai encore pleins d'id\u00e9es pour le rendre meilleur. Mais je suis content de rendre quelque chose qui fonctionne et qui est d\u00e9j\u00e0 techniquement utilisable sur le terrain. Ce projet m'a \u00e9galement appris pas mal de chose sur ma mani\u00e8re de travailler et sur la gestion de projet et je sais que tous mes futurs projets b\u00e9n\u00e9ficieront de ces apprentissages.","title":"Bilan"},{"location":"index.html#resume-des-epreuves","text":"Ici, je vais tenter de r\u00e9sumer tr\u00e8s rapidement tout ce qui a d\u00fb se passer pour en arriver l\u00e0. Pour commencer, il a fallu trouver un moyen de r\u00e9cup\u00e9rer des images de la F1TV automatiquement. Pour ce faire, j'ai d\u00fb trouver une librairie qui me permette de contr\u00f4ler un navigateur Firefox. Il a ensuite fallu trouver un moyen de se connecter automatiquement, pour ce faire, j'ai d\u00fb \u00e9crire un bout de code Python qui est all\u00e9 chercher les cookies dans la base de donn\u00e9es de chrome. Ensuite, il a fallu r\u00e9ussir \u00e0 naviguer sur la page de la F1TV en tenant compte des chargements et de la protection anti bots. Puis finalement trouver un moyen de retourner une image en assez bonne r\u00e9solution. Avec ces images, il a ensuite fallu d\u00e9velopper un syst\u00e8me qui permette \u00e0 l'utilisateur d'indiquer au programme o\u00f9 se trouvaient les informations. Il a ensuite fallu faire un syst\u00e8me qui utilise ces informations pour d\u00e9couper l'image pour isoler les infos et les envoyer \u00e0 la partie reconnaissance. Cette partie reconnaissance a d\u00fb \u00eatre d\u00e9velopp\u00e9e de mani\u00e8re quasi unique pour chaque type d'information reconnue et en plus de la partie reconnaissance qui \u00e9tait d\u00e9j\u00e0 bien gal\u00e8re, il a fallu faire tout un syst\u00e8me qui puisse d\u00e9tecter les anomalies de reconnaissances pour \u00eatre s\u00fbr que les informations r\u00e9cup\u00e9r\u00e9es \u00e9taient bonnes. Apr\u00e8s tout \u00e7a, il a fallu faire en sorte que ces donn\u00e9es soient stock\u00e9es et affich\u00e9es correctement. Cr\u00e9er une fa\u00e7on de les afficher de mani\u00e8re utile et facile \u00e0 l'utilisateur. Et tout ce beau monde a d\u00fb \u00eatre optimis\u00e9 pour que l'application ne prenne pas une minute pour r\u00e9cup\u00e9rer des images et il a fallu raccorder ensembles toutes les parties du projet en un seul qui fonctionne correctement sans crasher. C'est un r\u00e9sum\u00e9 un peu barbare qui oublie \u00e9norm\u00e9ment de choses et qui ne parle pas des probl\u00e8mes rencontr\u00e9s, mais cela peut donner une vague id\u00e9e de la taille du projet et de pourquoi je suis d\u00e9j\u00e0 si fier, juste que tout fonctionne. Merci d'avoir lu cette documentation, j'esp\u00e8re qu'elle a \u00e9t\u00e9 instructive et je vous souhaite une excellente journ\u00e9e","title":"R\u00e9sum\u00e9 des \u00e9preuves"},{"location":"index.html#notes-de-code","text":"Ici, je vais donner quelques petites infos qui pourraient vous \u00eatre utiles si vous d\u00e9cidez d'aller vous aventurer dans mon code source. Le programme n'est pas \u00e0 proprement parl\u00e9 un programme en MVC, le d\u00e9coupage g\u00e9n\u00e9ral suit quand m\u00eame cette philosophie, je vais donc les ranger de cette fa\u00e7on pour que \u00e7a soit plus simple pour vous de comprendre.","title":"Notes de code"},{"location":"index.html#vues","text":"Comme le projet n'est pas un MVC parfait, les vues font quand m\u00eame quelques actions, mais les deux fichiers dont je vais parler ici sont \u00e0 au moins 90\u2009% juste de la vue","title":"Vues"},{"location":"index.html#settingscs","text":"Ce fichier contient tout le code pour contr\u00f4ler la vue des \"Settings\" qui est la vue qui se charge de la cr\u00e9ation et \u00e9dition des Presets. Si vous voulez changer le comportement de cette page, il faut \u00e9diter ce fichier. Cette vue utilise deux contr\u00f4leurs : F1TVEmulator ConfigurationTool Le premier pour pouvoir lancer une instance de Firefox qui permet de tester le syst\u00e8me, le second pour effectuer toutes les actions de cr\u00e9ation, modification ou de lecture des \"Presets\" Rien de bien fou \u00e0 dire sur ce fichier. La seule chose un peu bizarre est la gestion de la cr\u00e9ation des zones et des fen\u00eatres. Il y a tout un syst\u00e8me qui peut \u00eatre un peu bizarre \u00e0 premi\u00e8re vue qui sert \u00e0 d\u00e9tecter quand l'utilisateur clique sur l'image pour cr\u00e9er une zone. Je suis s\u00fbr qu'il existe une mani\u00e8re plus propre de le faire que celle que j'ai utilis\u00e9e, mais j'ai fait en sorte que cela fonctionne. Un truc qui serait bien \u00e0 ajouter dans le futur serait un moyen de visualiser au moins les points que l'on ajoute au fur et \u00e0 mesure plut\u00f4t que de tout voir \u00e0 la fin.","title":"Settings.cs"},{"location":"index.html#form1cs","text":"Ce fichier contient tout le code pour contr\u00f4ler la vue principale. Elle se charge de lancer le navigateur et d'afficher toutes les donn\u00e9es r\u00e9cup\u00e9r\u00e9es ou stock\u00e9es. Cette vue utilise deux contr\u00f4leurs : F1TVEmulator DataWrapper Le premier pour contr\u00f4ler le navigateur (le lancer, le stopper, changer l'URL etc.) et le second pour acc\u00e9der \u00e0 des infos de la base de donn\u00e9e sans avoir \u00e0 l'appeler directement.","title":"Form1.cs"},{"location":"index.html#controleurs","text":"Ces classes ne sont pas des contr\u00f4leurs \u00e0 100\u2009%, car ils contiennent aussi un peu de calcul, etc. mais ont comme but principal de servir d'interface entre la vue et les donn\u00e9es.","title":"Contr\u00f4leurs"},{"location":"index.html#configurationtoolcs","text":"Cette classe sert \u00e0 travailler avec la zone principale pour la contr\u00f4ler et \u00e0 contenir les m\u00e9thode qui servent \u00e0 la cr\u00e9ation de Presets. Les deux grosses m\u00e9thodes que cette classe contient sont : SaveToJson AutoCalibrate La premi\u00e8re sert tout simplement \u00e0 prendre la configuration actuelle et la sauvegarder en format JSON dans un fichier dans le dossier PRESETS. La seconde prend une zone, utilise de l'OCR pour localiser les endroits o\u00f9 il y a du texte et fait une calibration auto pour cr\u00e9er automatiquement les zones de pilotes. Les autres m\u00e9thodes sont juste des m\u00e9thodes qui appellent des m\u00e9thodes de mod\u00e8les et servent seulement d'interface.","title":"ConfigurationTool.cs"},{"location":"index.html#datawrappercs","text":"Cette m\u00e9thode sert \u00e0 faire l'interm\u00e9diaire entre la form principale et le contr\u00f4ler \"Reader\" ainsi que la classe qui contr\u00f4le directement la base de donn\u00e9es. Elle interface avec ces deux classes : Reader Storage Reader est un genre d'hybride, mais qui se veut \u00eatre un genre de contr\u00f4ler de la lecture des donn\u00e9es sur les images et des fichiers JSON tandis que storage est le mod\u00e8le qui interagis directement avec la base de donn\u00e9es SQLITE. Cette classe contient des m\u00e9thodes qui auraient tr\u00e8s pu (et s\u00fbrement d\u00fbes) se retrouver directement dans la vue. La plupart des m\u00e9thodes sont l\u00e0 pour g\u00e9n\u00e9rer des contr\u00f4les qui contiennent des informations r\u00e9cup\u00e9r\u00e9es par la base de donn\u00e9es ou par l'OCR.","title":"DataWrapper.cs"},{"location":"index.html#readercs","text":"Cette m\u00e9thode est un genre d'hybride. Elle contient des calculs, etc. mais son but est de servir d'interface entre le reste du programme et les zones/fen\u00eatres de donn\u00e9es. C'est cette m\u00e9thode qui va g\u00e9rer la classe Zone, qui va demander \u00e0 la classe zone de modifier, ajouter ou supprimer des fen\u00eatres etc. Elle contient aussi des m\u00e9thodes pour charger un \"Preset\" et dessiner sur les Images quand une vue en a besoin.","title":"Reader.cs"},{"location":"index.html#zonecs","text":"Cette m\u00e9thode est clairement la plus discutable en tant que contr\u00f4leur, mais qui est en m\u00eame temps la plus proche. La raison est qu'elle peut \u00eatre deux choses. Une zone principale ou une zone de pilote. Dans le cas ou c'est une zone de pilote, c'est clairement une classe normale qui est utilis\u00e9e par un contr\u00f4leur et qui retourne des infos. Mais quand elle est utilis\u00e9e comme une zone principale, c'est l'orchestre de toutes les zones et fen\u00eatres. Dans ce dernier cas, c'est un interm\u00e9diaire entre les zones et fen\u00eatres. Elle ne sert qu'\u00e0 contr\u00f4ler des sous zones et leurs fen\u00eatres. Les seules m\u00e9thodes de cette classe servent \u00e0 demander des informations aux sous zones/fen\u00eatres. Il n'y a quasi aucun calculs.","title":"Zone.cs"},{"location":"index.html#modeles","text":"L\u00e0, on va parler des classes \"classiques\" (lol). Il y a deux types de classes dans cette liste : Les classes normales Les classes enfants de Window.cs Les classes normales sont ind\u00e9pendantes et contiennent toutes des m\u00e9thodes et des infos tr\u00e8s diff\u00e9rentes, tandis que les classes d\u00e9riv\u00e9es de Window.cs ont toutes la m\u00eame structure et ont comme seul et unique but de retourner ce qui est marqu\u00e9 dans leur image. Il est donc normal que ces derni\u00e8res se ressemblent beaucoup.","title":"Mod\u00e8les"},{"location":"index.html#driverdrswindowcs","text":"Cette classe est pr\u00e9vue pour contenir une image dans laquelle on peut voir l'\u00e9tat du DRS d'un pilote. La m\u00e9thode qu'elle utilise pour savoir si le pilote a activ\u00e9 son DRS ou non est d'utiliser la moyenne de couleur de son image. Elle retourne true ou false et elle contient elle-m\u00eame toutes les m\u00e9thodes qui sont n\u00e9cessaires pour donner une r\u00e9ponse (c'est un cas rare).","title":"DriverDrsWindow.cs"},{"location":"index.html#drivergaptoleaderwindowcs","text":"Cette classe est pr\u00e9vue pour contenir une image dans laquelle on peut voir combien de temps s\u00e9pare le pilote actuel du pilote devant lui. La m\u00e9thode qu'elle utilise pour le savoir utilise de l'OCR et fait appel \u00e0 une m\u00e9thode contenue dans son parent Window. Elle est plut\u00f4t vide, car tout le traitement est d\u00e9port\u00e9 dans son parent.","title":"DriverGapToLeaderWindow.cs"},{"location":"index.html#driverlaptimewindowcs","text":"Cette classe est pr\u00e9vue pour contenir une image dans laquelle on peut voir quel \u00e9tait le dernier temps au tour enregistr\u00e9 du pilote. La m\u00e9thode qu'elle utilise pour le savoir utilise de l'OCR et fait appel \u00e0 une m\u00e9thode contenue dans son parent. Elle est plut\u00f4t vide, car tout le traitement est d\u00e9port\u00e9 vers son parent.","title":"DriverLapTimeWindow.cs"},{"location":"index.html#drivernamewindowcs","text":"Cette classe est pr\u00e9vue pour contenir une image dans laquelle on peut voir le nom du pilote \u00e9crit en toutes lettres. La m\u00e9thode qu'elle utilise une partie d'OCR qui est d\u00e9port\u00e9e dans le parent et utilise aussi une m\u00e9thode appel\u00e9e IsADriver (qui aurait pu aussi \u00eatre d\u00e9port\u00e9e dans la page principale) qui v\u00e9rifie si le nom trouv\u00e9 existe.","title":"DriverNameWindow.cs"},{"location":"index.html#driverpositionwindowcs","text":"Cette classe est pr\u00e9vue pour contenir une image dans laquelle on peut voir la position d'un pilote. Cette m\u00e9thode est \u00e9galement un peu vide, car pour d\u00e9coder l'image le traitement est d\u00e9port\u00e9 dans son parent.","title":"DriverPositionWindow.cs"},{"location":"index.html#driversectorwindowcs","text":"Pareil que pour DriverPositionWindow.cs","title":"DriverSectorWindow.cs"},{"location":"index.html#drivertyreswindowcs","text":"Cette classe est pr\u00e9vue pour contenir une image dans laquelle on peut voir l'infographique qui repr\u00e9sente le pneu du pilote. Cette m\u00e9thode est la seule fen\u00eatre int\u00e9ressante, car elle utilise du code d\u00e9port\u00e9 dans le parent, mais aussi une certaine proportion qu'elle contient elle-m\u00eame. Elle contient des m\u00e9thodes qui permettent par exemple de trouver la zone int\u00e9ressante dans l'image ou choisir quel pneu un pilote chausse en fonction de la couleur moyenne de l'image de la zone trouv\u00e9e. Pour toutes les zones de type Window, ce qui est vraiment int\u00e9ressant, vous le trouverez dans le parent.","title":"DriverTyresWindow.cs"},{"location":"index.html#f1tvemulatorcs","text":"F1TVEmulator est la classe qui s'occupe de tout ce qui concerne le navigateur Headless. Cette classe utilise la librairie Selenium et est la pour tout faire. Elle s'occupe aussi bien d'envoyer la requ\u00eate, de cliquer sur un bouton apr\u00e8s 34 secondes que de r\u00e9cup\u00e9rer les cookies qui permettront de se connecter ensuite. Voici les m\u00e9thodes qui s'occupent des cookies : StartCookieRecovering GetCookie Je d\u00e9conseille de modifier ces deux m\u00e9thodes. Elles ont une utilit\u00e9 tr\u00e8s claire et elles fonctionnent. (If its not broken dont fix it) Ce qui peut \u00eatre int\u00e9ressant en revanche, c'est la seule autre m\u00e9thode que cette classe propose sobrement intitul\u00e9e \"Start\". Cette m\u00e9thode est cod\u00e9e de mani\u00e8re totalement proc\u00e9durale et d\u00e9crit exactement toutes les actions \u00e0 faire \u00e0 partir du moment ou le navigateur est d\u00e9marr\u00e9, dans quel ordre et s'il faut les faire ou non. Si vous vouliez modifier quelque chose ici, je pense que la bonne id\u00e9e serait une meilleure gestion des erreurs. Pour le moment, si le programme n'arrive pas \u00e0 cliquer sur certains boutons, soit une erreur est lanc\u00e9e, soit on attend un peu avant de r\u00e9essayer. La vraie chose qui manque, c'est la raison pour laquelle ces boutons n'ont pas pu \u00eatre cliqu\u00e9s. Dans l'id\u00e9al, il faudrait ajouter un syst\u00e8me qui peut d\u00e9tecter la panne exacte pour que le message d'erreur soit plus personnalis\u00e9. Sinon c'est une m\u00e9thode qui marche plut\u00f4t bien et qui est faite compl\u00e8tement sur mesure pour l'utilisation de la F1TV.","title":"F1TVEmulator.cs"},{"location":"index.html#ocrimagecs","text":"L\u00e0, on attaque les classes un peu plus \"bord\u00e9liques\". Cette classe regroupe toutes les actions de filtrage que l'on pourrait vouloir. Cette classe est pas mal utilis\u00e9e pour l'OCR. Il n'y a que deux choses \u00e0 savoir. Presque toutes les m\u00e9thodes de filtres sont g\u00e9n\u00e9riques et peuvent \u00eatre utilis\u00e9es \u00e0 peu pr\u00e8s n'importe o\u00f9 et n'importe quand et devraient toujours fonctionner tant qu'on leur fournit ce dont elles ont besoin (la plupart sont en statique). La seule m\u00e9thode qui va vous int\u00e9resser si vous voulez changer le comportement de l'OCR est la m\u00e9thode \"Enhance\". La m\u00e9thode enhance est un genre de mode d'emploi. Selon le contexte de l'image (si c'est une image qui vient d'une fen\u00eatre de DRS, de temps au tour, de pneu etc.) il y aura une combinaison de filtres diff\u00e9rente. Plusieurs m\u00e9thodes dans cette classe ne sont pas utilis\u00e9es, mais sont gard\u00e9es, car elles pourraient \u00eatre utiles. La plupart du temps, l'utilisation de ces filtres est d\u00e9cid\u00e9e avec des essais \u00e0 t\u00e2tons. Vous comprendrez donc vite que c'est mieux de garder sous le code des m\u00e9thodes car certaines combinaisons marchent mieux que d'autres.","title":"OcrImage.cs"},{"location":"index.html#sqlitestoragecs","text":"Cette classe est plut\u00f4t simple. Ce sont simplement toutes les m\u00e9thodes qui permettent de cr\u00e9er, \u00e9diter et acc\u00e9der \u00e0 la base de donn\u00e9es SQLITE. Vous y trouverez des m\u00e9thodes qui sont juste l\u00e0 pour cr\u00e9er la base comme d'autres plus sp\u00e9cifiques qui sont un peu plus sp\u00e9cifiques comme celles qui veulent r\u00e9cup\u00e9rer l'ID d'un pilote selon son nom ou celle qui veut r\u00e9cup\u00e9rer l'historique des temps autour d'un pilote. Rien de sp\u00e9cial \u00e0 dire sur cette classe.","title":"SqliteStorage.cs"},{"location":"index.html#windowcs","text":"Ahlala... je pense que c'est une des classes les plus longues de tout le projet. Du haut de ses presque 700 lignes, cette classe s'occupe de tout ce que les enfants fen\u00eatres pourraient avoir besoin. On retrouve des m\u00e9thodes pour calculer la diff\u00e9rence entre deux strings qui peut servir pour aider \u00e0 la reconnaissance de noms de pilotes ou bien une m\u00e9thode qui permet de convertir une image en tableau de bytes. La m\u00e9thode la plus grosse cependant et de loin est la m\u00e9thode GetTimeFromPng qui doit impl\u00e9menter un syst\u00e8me qui permet de d\u00e9tecter quand un temps est anormal et d\u00e9tecter si la raison est la mauvaise compr\u00e9hension d'une ponctuation ou le rajout d'un chiffre. Cela prend \u00e9norm\u00e9ment de place, car il y a beaucoup de cas particuliers et il a fallu tout coder \u00e0 la main. Je d\u00e9conseille \u00e0 qui que ce soit de lire cette m\u00e9thode, ainsi, elle pourrait causer de s\u00e9v\u00e8res dommages au cerveau humain. \u00c0 \u00e9crire, ce fut une horreur, \u00e0 comprendre, je n'ose pas imaginer. Sinon pas grand-chose de plus \u00e0 raconter.","title":"Window.cs"},{"location":"index.html#structures","text":"Les classes de structures sont des classes qui ne contiennent que peu ou pas de traitement et qui sont simplement l\u00e0 pour contenir des informations. Elles sont pratiques, car elles permettent de rendre le code dans les autres classes beaucoup plus lisible et leur \u00e9viter d'utiliser des tuples bizarres.","title":"Structures"},{"location":"index.html#driverdatacs","text":"Cette classe contient toutes les infos d'un pilote \u00e0 un moment donn\u00e9. On peut voir cette classe comme une classe contenant une ligne de la F1TV. Toutes les donn\u00e9es \u00e0 propos d'un pilote que l'on peut d\u00e9tecter en une d\u00e9tection sont stock\u00e9es l\u00e0-dedans. Il n'y a pas de notion d'historique ou quoi que ce soit. C'est simplement un moyen de stocker des donn\u00e9es de pilotes dans d'autres classes en ayant un nom logique et aider \u00e0 la lecture. Pas r\u00e9ellement de traitement. Ce fichier contient \u00e9galement un autre objet : Tyre. Cet objet contient les infos d'un pneu, rien de plus. Et voil\u00e0, ce fut un r\u00e9sum\u00e9 extr\u00eamement succinct de tous les fichiers .CS de l'application pour que vous sachiez ce que vous regardez quand vous irez voir dans la partie code source de la documentation. Normalement, il devrait y avoir aussi un certain nombre de commentaires dans ces fichiers pour expliquer certains choix un peu bizarres. C'est en anglais, mais pas de l'anglais tr\u00e8s difficile \u00e0 comprendre. Bonne chance !","title":"DriverData.cs"},{"location":"index.html#glossaire","text":"Vocabulaire F1 : DRS : Drag Reduction System. : Syst\u00e8me qui permet d'ouvrir l'aileron arri\u00e8re de la monoplace quand elle se trouve \u00e0 une seconde ou moins de la voiture devant elle. Cela permet de r\u00e9duire la train\u00e9e que la voiture subit et lui permet d'avoir un petit boost qui aide \u00e0 d\u00e9passer. Pitstop : Arr\u00eat aux stands : Pendant une course de F1, les pneus s'usent extr\u00eamement vite et tous les pilotes sont oblig\u00e9s de passer au moins une fois par les stands par course pour les changer. Et pour changer ces pneus, ils font un arr\u00eat aux stands que l'on appelle dans le milieu un Pitstop. Pneus Hard, Medium, Soft, Inter, Wet : Types de pneus de F1. Hard est un pneu qui ne s'use pas beaucoup, mais qui est lent, Soft est l'inverse et Medium est l'entre deux. Les pneus sont des outils strat\u00e9giques et il est tr\u00e8s important de savoir lequel chaque pilote utilise. Les pneus Inter et Wet sont des pneus pluies, l'Inter \u00e9tant pour les faibles pluies. Secteur : Section de circuit : Les circuits de F1 sont toujours d\u00e9coup\u00e9s en trois parties qui sont mesur\u00e9es s\u00e9par\u00e9ment et qui permettent une meilleure granularit\u00e9 dans l'estimation des r\u00e9sultats. On n'est pas oblig\u00e9 d'attendre la fin d'un tour pour savoir si un pilote est rapide ou non et on peut voir dans quelle partie du circuit, il est rapide ou lent. Monoplace : Voiture \u00e0 une seule place, terme utilis\u00e9 souvent pour d\u00e9crire les F1 dans le document. Grand Prix : Course officielle de Formule 1. \u00c9v\u00e9nement faisant partie du championnat du monde de Formule 1. Vocabulaire projet : Browser/navigateur Headless : Navigateur qui existe et fonctionne sans interface graphique OCR : Optical Character Recognition : Processus de reconnaissance de texte sur une image par un ordinateur TDD : Test Driver Developppement : D\u00e9veloppement avec pour objectif les test. Les tests sont \u00e9crits en amont et le but du d\u00e9veloppeur est simplement de les faire passer. MVC : Mod\u00e8le Vue Controlleur : Architecture de projet qui s\u00e9pare le traitement de l'information, son affichage et sa gestion. Preset : (dans ce projet) Set d'informations pr\u00e9par\u00e9es \u00e0 l'avance pour \u00eatre utilis\u00e9s ult\u00e9rieurement. DB : Data Base / Base de donn\u00e9e Cookie : Fichier cr\u00e9\u00e9 par un site internet stock\u00e9 sur la machine du client qui est utilis\u00e9 en g\u00e9n\u00e9ral pour conserver des informations de connexion m\u00eame apr\u00e8s la fermeture du navigateur. Window : Fen\u00eatre (dans ce projet) objet contenant une partie d'une image contenant une information pr\u00e9cise. Zone : (dans ce projet) objet contenant une partie d'une image qui peu \u00eatre sous divis\u00e9e en fen\u00eatres de donn\u00e9es. Wrapper : Code qui s'occupe de faire l'interface entre une librairie ou une classe pour rendre l'utilisation plus simple ou plus propre JSON : JavaScript Object Notation : Format de fichier qui permet de stocker des informations dans un format tr\u00e8s pr\u00e9cis AWS : Amazon Web Service : Service d'h\u00e9bergement d'Amazon User Agent : Signature num\u00e9rique du navigateur qui permet \u00e0 un site de d\u00e9tecter le type d'appareil et de navigateur connect\u00e9 (peut \u00eatre chang\u00e9 manuellement) CSV : Comma Separated Values : Format de fichier qui permet de stocker facilement des donn\u00e9es sous forme de tableau API : Application Programming Interface : Interface g\u00e9n\u00e9rique qui permet d'acc\u00e9der \u00e0 une ressource.","title":"Glossaire"},{"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":"ManuelUtilisateur.html","text":"Manuel utilisateur Installation Pour Installer le projet, il faut aller sur le repo qui contient le projet et suivre les \u00e9tapes indiqu\u00e9es dans le ReadMe. Les \u00e9tapes ne sont pas r\u00e9p\u00e9t\u00e9es ici pour \u00e9viter de la redondance et pour \u00e9viter de devoir changer cette documentation si les versions ou les d\u00e9pendances \u00e0 installer changent. A quoi sert l'app ? Track Trends est une application Windows qui sert d'outil pour des abonn\u00e9s de la F1TV qui veulent exploiter le potentiel de la page DATA de la F1TV. L'application est faite pour un seul cas d'utilisation : R\u00e9cup\u00e9rer les infos d'un Grand Prix en cours ou d'une rediffusion et les afficher de mani\u00e8re plus int\u00e9ressante (en mettant en valeur des infos, en construisant un historique et en en interpr\u00e9tant d'autres). L'application est faite pour que l'utilisateur ait le moins de choses \u00e0 faire pour que tout fonctionne. Cela ne veut pas dire que l'utilisateur n'a rien \u00e0 faire et le but de ce document est d'expliquer les diff\u00e9rentes \u00e9tapes n\u00e9cessaires \u00e0 l'utilisation de l'application. Utilisation \"Simple\" Le but de cette section est de vous permettre d'apprendre \u00e0 lancer, stopper et modifier les variables de fonctionnement de l'application. Vous serez donc en mesure d'utiliser le programme correctement. Vous devez commencer par lancer le programme. \"Page de lancement\" Au lancement de l'app, vous devriez avoir une page comme ci-dessus. Il y a beaucoup d'infos, mais pour le moment ce qui nous int\u00e9resse, c'est le menu en haut \u00e0 gauche. \"Contr\u00f4les\" Si c'est la premi\u00e8re fois que vous utilisez l'application, il est possible qu'il n'y ait qu'un seul preset au lieu des quatre ci-dessous voire 0 si vous n'avez pas mis le preset inclus dans l'installation. Si vous n'avez aucun preset, passez directement \u00e0 l'\u00e9tape calibration avant de revenir ici. Comme vous pouvez le voir, en plus des presets s\u00e9lectionnables, il y a quatre boutons (dont deux gris\u00e9s) et une boite de texte avec un lien. Le lien est la chose la plus simple. C'est le lien du Grand Prix que vous voulez analyser. R\u00e9cup\u00e9rer le lien vers le flux Pour remplir la boite de texte pr\u00e9cit\u00e9e, il nous faut un lien et ce lien, on le trouve sur la F1TV . Quand vous vous rendez sur la F1TV la page principale devrait ressembler \u00e0 quelque chose comme \u00e7a (\u00e9videmment le contenu ne sera pas le m\u00eame) : \"Page principale de la F1TV\" Si vous ne vous \u00eates pas d\u00e9j\u00e0 connect\u00e9, connectez-vous et ensuite, allez sur la page du Grand Prix qui vous int\u00e9resse. Pour un Grand Prix en direct, normalement, il y a un endroit o\u00f9 vous pouvez cliquer pour acc\u00e9der au flux directement, et si vous voulez utiliser un Grand Prix en diff\u00e9r\u00e9, vous pouvez aller dans \"202x Season\" ou x est l'ann\u00e9e en cours, en l'occurrence 2023. Et l\u00e0, vous pourrez s\u00e9lectionner un Grand Prix. Dans tous les cas, quand vous avez s\u00e9lectionn\u00e9 votre Grand Prix, vous devriez vous retrouver sur une page de ce style : \"Page d'un Grand Prix sur la F1TV\" Et ici si vous \u00eates sur un Grand Prix en direct, vous pouvez directement copier l'URL de la page et si c'est un Grand Prix en diff\u00e9r\u00e9, vous pouvez directement naviguer avec la barre de progression vid\u00e9o du lecteur de la F1TV, Je conseille de la placer juste apr\u00e8s le d\u00e9part. Vous pouvez prendre le lien avant ou apr\u00e8s avoir chang\u00e9 le timecode de la vid\u00e9o, cela ne change rien, le lien ne contient pas votre progression dans la vid\u00e9o. Ensuite, vous pourrez juste mettre le lien que vous avez r\u00e9cup\u00e9r\u00e9 dans la boite de texte que l'on a vu pr\u00e9c\u00e9demment. Il faut simplement s'assurer que le lien contient bien le terme ?action=play \u00e0 la fin. Boutons Vous n'avez acc\u00e8s qu'\u00e0 deux boutons au lancement de l'application. Le premier nomm\u00e9 'Config' et le second 'Launch' Le bouton 'Config' permet d'ouvrir la page de calibration (voir section Calibration). Le bouton Launch permet de lancer le programme. Le texte peut changer (Par exemple en 'Retry' si le lancement a rat\u00e9) mais son utilit\u00e9 reste la m\u00eame : Lancer le navigateur virtuel qui sera requis pour traiter les donn\u00e9es. Si vous cliquez sur Launch tous les boutons devraient \u00eatre gris\u00e9s, c'est normal. Pendant le lancement du navigateur, l'application ne peut pas \u00eatre utilis\u00e9e pour \u00e9viter des conflits. Il faut juste attendre. Vous aurez \u00e9galement des fen\u00eatres d'invite de commande qui vont s'ouvrir et se fermer, c'est tout \u00e0 fait normal et il ne faut pas interagir avec tant que tout ne s'est pas stabilis\u00e9. Vous devriez voir enfin une fen\u00eatre comme celle ci : \"Page suspecte (Pas de panique, elle ne l'est pas)\" Il ne faut pas fermer cette fen\u00eatre. C'est la seule chose qui nous relie au navigateur virtuel qu'utilise l'application. Il ne faut pas la fermer, par contre, vous pouvez la r\u00e9duire et la laisser en background, c'est d'ailleurs ce que je vous recommande de faire. Peu apr\u00e8s l'ouverture de cette fen\u00eatre, vous devriez avoir acc\u00e8s \u00e0 trois boutons. Bouton 'Config' qui fait la m\u00eame chose qu'avant Bouton 'Re Launch' qui remplace le bouton 'Launch' qui permet de relancer le processus dans le cas ou vous auriez envie de changer de lien ou de preset. Bouton 'Start' lance le processus de r\u00e9cup\u00e9ration de donn\u00e9es. Si vous cliquez sur ce dernier, vous lancerez le programme. Cela peut prendre quelque secondes \u00e0 s'actualiser, mais tr\u00e8s vite vous devriez vous trouver devant une page comme celle-ci : \"D\u00e9but du programme\" Vous n'aurez alors qu'un seul bouton disponible : le bouton 'Stop'. Vous pouvez arr\u00eater le programme quand vous le voulez. ATTENTION : Avant de fermer le programme, merci de stopper d'abord le processus. Si vous stoppez le processus, vous devriez vous retrouver dans la m\u00eame situation que juste apr\u00e8s avoir lanc\u00e9 le navigateur virtuel : \"Arr\u00eat du programme\" Vous pouvez voir en dessous la derni\u00e8re image que le programme a d\u00e9cod\u00e9e et vous avez de nouveau acc\u00e8s aux trois boutons expliqu\u00e9s pr\u00e9c\u00e9demment. Quand l'application a \u00e9t\u00e9 lanc\u00e9e, vous pouvez aussi cliquer sur les donn\u00e9es affich\u00e9es, mais ces comportements sont expliqu\u00e9s dans la section \"Comprendre les donn\u00e9es\" Voil\u00e0 ! Vous savez comment lancer, stopper et modifier les variables de fonctionnement de l'application. Maintenant, vous pouvez passer directement \u00e0 la section \"Comprendre les donn\u00e9es\" si vous voulez apprendre comment utiliser ce que propose le programme ou passer \u00e0 la section \"Calibration\" si vous voulez apprendre \u00e0 configurer l'application et cr\u00e9er des Presets. Calibration Ici, vous apprendrez \u00e0 configurer l'application pour cr\u00e9er vos propres presets et surtout pourquoi vous auriez besoin de le faire et \u00e0 quoi servent ces fameux \"Preset\". Vous serez en mesure d'adapter le fonctionnement de l'application, qu'elle soit conforme aux diff\u00e9rents changements d'interface et de pilotes pr\u00e9sents. Pourquoi calibrer l'application et cr\u00e9er un Preset ? Le programme que vous allez utiliser utilise de l'OCR (Optical Character Recognition). Cela veut dire que le programme va aller regarder chaque image de la F1TV et convertir ce qu'il y a marqu\u00e9 dessus en donn\u00e9es que l'on peut r\u00e9cup\u00e9rer automatiquement. Sauf que pour y arriver, le programme a besoin de savoir o\u00f9 se trouve le texte important et surtout quel type de donn\u00e9e, il est cens\u00e9 trouver \u00e0 tel ou tel endroit. Pour \u00eatre plus pr\u00e9cis, il a \u00e9galement besoin de savoir quels pilotes peuvent se trouver dans la course actuelle. Et pour qu'il sache tout cela, il y a des fichiers de configuration nomm\u00e9s \"Presets\" qui doivent contenir toutes ces informations. Et pour les cr\u00e9er, il existe la page de calibration qui va demander \u00e0 un humain de lui fournir les infos qu'il ne peut pas trouver par lui-m\u00eame. Normalement, il devrait y avoir un Preset fournis par l'application quand vous la t\u00e9l\u00e9chargez depuis le repo mais vous pouvez avoir besoin de faire votre propre \"Preset\" dans deux cas : Quand les pilotes pr\u00e9sents ne sont pas les m\u00eames (Ex : Si un pilote doit \u00eatre remplac\u00e9 ou si de nouveaux pilotes sont arriv\u00e9s ou bien que d'autres aient pris leur retraite) Quand vous voulez utiliser des Grand Prix fait d'une autre ann\u00e9e que celle pour laquelle le \"Preset\" original a \u00e9t\u00e9 pr\u00e9vu. (L'ann\u00e9e devrait \u00eatre mentionn\u00e9e dans le nom du preset) Il est d\u00e9conseill\u00e9 d'utiliser ce projet pour des Grand Prix couru avant 2022, car l'interface de la page Data de la F1TV peut avoir trop chang\u00e9, mais vous pouvez toujours essayer pour voir en faisant votre propre Preset pour des ann\u00e9es comme 2021 ou 2020. Comment cr\u00e9er son Preset Pour cr\u00e9er son Preset, il faut utiliser la page Settings ou Config de l'application. Pour y acc\u00e9der, il faut simplement cliquer sur le bouton \"Config\" de la page principale et vous serez accueilli par une page qui devrait ressembler \u00e0 celle ci dessous : \"Page de config vide\" La seule diff\u00e9rence est que vous aurez s\u00fbrement moins de Presets dans la liste des Presets voir, vous pouvez n'en avoir aucuns. Comme vous pouvez le voir, vous n'avez pas \u00e9norm\u00e9ment d'options disponibles quand vous lancez simplement l'application. Vous n'avez acc\u00e8s qu'aux boutons \"Start the browser\" et \"Add\",\"Remove\". On verra \u00e0 quoi servent ces deux derni\u00e8res plus tard, mais dans un premier temps, on va se pencher sur le bouton Start. Vous pouvez voir qu'en plus de ce bouton, vous pouvez aussi rentrer du texte dans la boite de texte \"Grand Prix URL\" ce qui n'est pas un hasard. Le but, c'est de mettre le lien vers un Grand Prix pour pouvoir avoir une image de test. Pour voir comment r\u00e9cup\u00e9rer cet URL, voir la section (Utilisation \"simple\" \u2192 \"R\u00e9cup\u00e9rer le lien vers le flux\") Quand l'URL a \u00e9t\u00e9 plac\u00e9e au bon endroit, on peut cliquer sur le bouton \"Start\" et attendre. Si vous n'avez pas encore lu la partie \"Utilisation \"simple\"\", vous allez voir des invites de commandes s'ouvrir et se fermer et c'est tout \u00e0 fait normal. Il faut attendre d'avoir une invite de commande qui reste ouverte et qui ressemble \u00e0 celle ci dessous avant de faire quoi que ce soit. Ensuite, vous pourrez simplement r\u00e9duire cette fen\u00eatre et retourner sur la page de configuration. \"Invite de commande suspecte, mais normale\" Ensuite, si vous attendez un peu (le processus peut prendre jusqu'\u00e0 plusieurs dizaines de secondes), vous aurez soit une erreur, soit vous vous retrouverez avec une page qui ressemble \u00e0 \u00e7a : \"Page de config amorc\u00e9e\" Et l\u00e0 on voit que toutes les options ont \u00e9t\u00e9 d\u00e9bloqu\u00e9es. Avant de se pencher sur les nouvelles options, on va rester sur le fonctionnement g\u00e9n\u00e9ral. Le bouton \"Get a newer image\" permet d'avoir une image plus r\u00e9cente de la F1TV. C'est utile par exemple, car comme on le voit sur l'exemple, l'interface de la F1TV n'a pas encore eu le temps de disparaitre. Mais cela peut aussi \u00eatre utile plus tard dans le cas o\u00f9 vous voudriez que certaines infos s'affichent. Le bouton \"Reset Emulator\", lui, permet, comme son nom l'indique, de relancer le navigateur virtuel que l'on a lanc\u00e9 avec le bouton \"Start the browser\". Cela peut \u00eatre utile dans le cas ou l'image ne soit pas l'image que l'on attendait ou si on veut tenter le coup avec un autre URL. Maintenant, on peut passer \u00e0 la cr\u00e9ation des zones et des windows Cr\u00e9ation des Zones et des Windows Sans trop rentrer dans les d\u00e9tails, l'application a besoin de connaitre la localisation de certains \u00e9l\u00e9ments. Elle a besoin de savoir : O\u00f9 se trouve la zone g\u00e9n\u00e9rale des infos pilotes O\u00f9 se trouve chaque pilote O\u00f9 se trouvent les informations pour chacuns de ces pilotes Le programme de calibration est fait pour que vous n'ayez besoin de donner que les informations qu'il ne peut pas deviner. Il y a deux \u00e9tapes : La premi\u00e8re \u00e9tape est de donner les dimensions de la zone principale d'informations. Pour ce faire, il faut dans un premier temps cliquer sur le bouton \"Create the main zone\" qui devrait afficher le texte suivant en dessous apr\u00e8s avoir cliqu\u00e9 : \"Texte indiquant le nombre de points qu'il reste \u00e0 ajouter\" Ensuite, il va falloir cliquer directement sur l'image pour indiquer le coin en haut \u00e0 gauche et le point en bas \u00e0 droite de la zone rectangulaire qui contient les informations. Vous pouvez voir en bleu ci-dessous les coins du rectangle et en rouge l'endroit o\u00f9 il faut cliquer. \"Infographie expliquant o\u00f9 placer les points de la zone principale\" Il faut faire attention \u00e0 bien prendre tous les pilotes MAIS il faut \u00e9galement faire attention \u00e0 ne pas prendre le texte alentours. \"Texte qui ne doit pas \u00eatre dans la zone s\u00e9lectionn\u00e9e\" Ci dessus, on peut voir du texte barr\u00e9 en violet. Il ne faut surtout pas que la zone vienne inclure ces bouts de texte ou toute la calibration pourrait rater. Ensuite, si vous avez bien fait votre travail, au deuxi\u00e8me clic sur la page, vous aurez quelques secondes de flottement et ensuite, vous devriez avoir les contours que vous avez dessin\u00e9s affich\u00e9s en jaune avec pleins de plus petites zones \u00e0 l'int\u00e9rieur comme ci-dessous : \"Zone principale avec les zones de pilotes automatiquement calcul\u00e9es\" Le programme a non seulement pris en compte la zone, mais il a aussi d\u00e9tect\u00e9 automatiquement o\u00f9 se trouvaient les zones de chaque pilote. Si vous n'avez pas un r\u00e9sultat comme celui-l\u00e0 et/ou que les zones ne sont pas bien align\u00e9es sur les pilotes sur l'image, je vous conseille de r\u00e9essayer de cr\u00e9er la zone principale. La seconde \u00e9tape est de montrer o\u00f9 sont les fen\u00eatres d'infos au programme. Vous aurez peut-\u00eatre remarqu\u00e9 qu'en dessous de l'image principale, quand l'affichage jaune a \u00e9t\u00e9 appliqu\u00e9, une image est apparue. Elle devrait ressembler \u00e0 quelque chose dans ce style : \"Image d'une zone de pilote\" C'est une zone de pilote de l'image que l'on voit au-dessus et c'est ici que l'on va indiquer les zones int\u00e9ressantes. Pour ce faire, il faut cliquer sur le bouton \"Create Windows\" et ce message devrait s'afficher un peu en dessous : \"Texte indiquant le nombre de windows \u00e0 ajouter\" Cela nous indique le nombre de fen\u00eatres qu'il nous reste \u00e0 s\u00e9lectionner. La technique est la m\u00eame que pour la grande zone sauf que l\u00e0, il faut le faire neuf fois. Et il faut absolument le faire dans l'ordre de gauche \u00e0 droite. Le but est de tout s\u00e9lectionner et d'arriver \u00e0 ce r\u00e9sultat : \"Exemple de fen\u00eatres d\u00e9coup\u00e9es\" Dans l'ordre, de gauche \u00e0 droite, on veut r\u00e9cup\u00e9rer : La position du pilote Son \u00e9cart avec le leader (en l'occurrence comme c'est le leader que l'on voit, on doit s\u00e9lectionner la fen\u00eatre ou il est marqu\u00e9 \"LEADER\") Son dernier temps au tour La fen\u00eatre DRS L'\u00e9tat de ses pneus Son nom Son temps au secteur 1 Son temps au secteur 2 Son temps au secteur 3 Ajout du nom des pilotes La derni\u00e8re info qui manque \u00e0 notre programme est la liste des pilotes pr\u00e9sents. On peut interagir avec cette liste ici : \"Menu d'interaction avec la liste de pilotes\" Dans la boite de texte, on peut \u00e9crire le nom d'un pilote qui peut \u00eatre trouv\u00e9 sur l'image, on peut l'ajouter \u00e0 la liste et si on a fait une erreur, on peut le retirer. Et c'est \u00e0 peu pr\u00e8s tout. Il ne reste maintenant plus qu'\u00e0 mettre tous les noms. TIP : Il peut \u00eatre int\u00e9ressant d'ajouter le nom des pilotes de r\u00e9serve pour \u00e9viter de venir changer son preset si un pilote n'est pas pr\u00e9sent pour cause de maladie ou de blessure. Il faut simplement faire attention de ne pas non plus mettre trop de noms pour \u00e9viter que le programme puisse confondre. Et voil\u00e0 ! On a toutes les infos n\u00e9cessaires. La page de configuration devrait ressembler \u00e0 \u00e7a : \"Image de la page de config apr\u00e8s avoir ajout\u00e9 toutes les infos\" Il ne reste plus qu'\u00e0 le sauvegarder. Sauvegarder le nouveau Preset Pour sauvegarder le preset, on peut facilement lui donner un nom dans la zone de texte sous les trois boutons de contr\u00f4le des Presets et cliquer sur Save current Preset. Et voil\u00e0, vous savez d\u00e9sormais comment cr\u00e9er vos propres presets et \u00e0 quoi ils servent. Vous pouvez donc adapter le fonctionnement du projet pour qu'il soit conforme aux changements de pilotes et d'interface avec les ann\u00e9es. Vous pouvez ainsi utiliser l'application normalement en s\u00e9lectionnant votre nouveau preset \u00e0 chaque fois. Load un Preset existant Le loading est un peu sp\u00e9cial. Il faut d\u00e9j\u00e0 avoir lanc\u00e9 le navigateur virtuel pour activer les boutons. Ensuite, il suffit de s\u00e9lectionner un preset et de cliquer sur \"Load the Preset\" et attendre un petit peu. L'affichage sera un peu bizarre, mais c'est normal, il ne faut pas s'inqui\u00e9ter si la zone de pilote affich\u00e9e en dessous est correcte. \"Exemple de ce \u00e0 quoi peut ressembler un loading et l'affichage bizarre que \u00e7a implique\" \u00c0 partir de l\u00e0, il est facile de faire son propre preset en changeant juste les noms des pilotes par exemple. Comprendre les donn\u00e9es Ici, vous allez apprendre \u00e0 utiliser l'application et ses donn\u00e9es en comprenant \u00e0 quoi elles servent et comment elles sont affich\u00e9es. \"Image de l'application en cours de fonctionnement depuis quelques minutes\" Il y a 5 types de donn\u00e9es : Overtakes Cette fen\u00eatre est loin d'\u00eatre compl\u00e8tement op\u00e9rationnelle, mais elle permet de voir l'historique des d\u00e9passements et des changements de position. Il faut scroller pour voir les plus r\u00e9cents. Je dirais que pour le moment, c'est la moins int\u00e9ressante et elle ne fonctionne pas toujours super. Last Five Laps \"Image de la fen\u00eatre des cinq derniers tours\" En total contraste avec la fen\u00eatre Overtakes, ici, on a peut-\u00eatre la fen\u00eatre la plus int\u00e9ressante de toutes. On peut voir les pilotes actuellement les plus rapides et plus lents sur le circuit. Le calcul est fait sur la moyenne des cinq derniers tours de chaque pilote. Cela permet de se faire une id\u00e9e de la situation des pilotes. Cela peut servir par exemple \u00e0 d\u00e9tecter quand un pneu est plus rapide que les autres quand on voit des pilotes dans le milieu ou bas de tableau appara\u00eetre dans le plus rapide. Il est par exemple int\u00e9ressant de voir la diff\u00e9rence de vitesse entre les pneus secs et pluie sur un circuit qui commence \u00e0 s\u00e9cher. On peut progressivement voir les pilotes en pneus secs devenir de plus en plus rapides alors que ce sont des pilotes beaucoup moins rapide en temps normal. On peut cliquer sur n'importe lequel de ces pilotes pour voir ses infos appara\u00eetre dans la fen\u00eatre Driver Infos pour voir les cinq derniers tours par exemple ou les pneus qu'il est en train de chausser. Battles \"Fen\u00eatre des batailles\" Cette fen\u00eatre est \u00e9galement assez int\u00e9ressante, car elle permet de voir les pilotes qui sont en train de se battre. Il est estim\u00e9 qu'un pilote qui se bat est un pilote qui est \u00e0 trois secondes ou moins du pilote devant lui. Ne sont affich\u00e9es que les batailles de quatre pilotes maximums dans l'ordre du classement. Si un pilote est entre 2 et 3 secondes de son adversaire, l'\u00e9cart est en blanc. Entre 1 et 2 secondes, il est \u00e9crit en jaune. Dans la zone du DRS (Une seconde ou moins) il est \u00e9crit en vert. On peut aussi cliquer sur le nom d'un pilote pour afficher ses infos dans la fen\u00eatre des infos pilote. Cette fen\u00eatre permet de mieux comprendre qui sont les pilotes qu'il faut garder \u00e0 l'\u0153il \u00e0 la TV ou simplement qui sont les pilotes qui sont tr\u00e8s proches et qui pourraient se mettre la pression pour les arr\u00eats aux stands, car jamais un pilote ne reste \u00e0 moins de trois secondes d'un autre si \u00e7a n'est pas pour tenter quelque chose au niveau strat\u00e9gique. Driver infos \"Fen\u00eatre des infos pilote\" Ici, on peut voir toutes les infos lives d'un pilote. Rien de fou \u00e0 dire sur la partie de gauche, par contre la partie de droite est un peu plus int\u00e9ressante, car elle contient un historique de ses cinq derniers tours (dans la photo, il n'y en a qu'un seul, mais au fur et \u00e0 mesure de la course cela se remplit). On peut non seulement voir les cinq derniers temps au tour, mais on peut aussi cliquer sur chacun d'eux pour voir les secteurs associ\u00e9s. \"Exemple d'affichage des secteurs d'un temps au tour\" ; Live Ranking \"Fen\u00eatre du classement en direct\" Ceci est la fen\u00eatre la plus simple. C'est tout b\u00eatement le classement actuel avec les \u00e9carts avec le leader. Note : On peut \u00e9galement cliquer sur les diff\u00e9rents pilotes pour en afficher les infos dans la fen\u00eatre infos pilote. Erreurs Il est tr\u00e8s probable que si vous utilisez beaucoup cette application, vous allez rencontrer des erreurs. Ici, vous pourrez apprendre ce qu'elles veulent dire et ce que vous pouvez faire pour y rem\u00e9dier. Il n'existe pas un tr\u00e8s grand nombre d'erreurs, mais voici les principales Erreur 100 (Souvent au premier d\u00e9marrage) Cette erreur signale un probl\u00e8me avec la r\u00e9cup\u00e9ration de cookies. Cela peut \u00eatre caus\u00e9 par une mauvaise installation de python ou si vous ne vous \u00eates pas connect\u00e9s r\u00e9cemment \u00e0 la F1TV depuis Chrome ou que vous n'avez tout simplement pas install\u00e9 Chrome sur votre machine. Erreur 101 (Moins r\u00e9current qu'\u00e0 une \u00e9poque) Cette erreur veut dire qu'il y a d\u00e9j\u00e0 une instance de navigateur ouverte. Pour r\u00e9gler cette erreur, regarder dans votre barre des t\u00e2ches les invites de commande ouvertes et fermez celui qui correspond \u00e0 un ancien navigateur. (Si c'est d\u00e9j\u00e0 fait alors en derniers recours, vous pouvez chercher dans le gestionnaire des t\u00e2ches et chercher \"GeckoDriver.exe\") Erreur 102 Cela peut \u00eatre une erreur qui arrive, car vous n'avez pas donn\u00e9 un URL valide pour la F1TV ou par ce que vous ne vous \u00eates pas connect\u00e9 r\u00e9cemment \u00e0 la F1TV depuis chrome (Si vous veniez de le faire alors, attendez un peu et r\u00e9essayez). Parfois cela peut prendre un peu de temps \u00e0 s'actualiser). Erreur 103 L'URL est invalide Erreur 104 L'URL est invalide Erreur 105 Cette erreur indique que soit vous avez donn\u00e9 un URL qui ne correspond \u00e0 aucun Grand Prix, soit que la vid\u00e9o a mis trop de temps \u00e0 charger. Vous pouvez essayer de vous brancher en Ethernet ou simplement r\u00e9essayer si vous \u00eates s\u00fbr de votre URL. Erreur 106 M\u00eame chose que pour la 105","title":"Manuel utilisateur"},{"location":"ManuelUtilisateur.html#manuel-utilisateur","text":"","title":"Manuel utilisateur"},{"location":"ManuelUtilisateur.html#installation","text":"Pour Installer le projet, il faut aller sur le repo qui contient le projet et suivre les \u00e9tapes indiqu\u00e9es dans le ReadMe. Les \u00e9tapes ne sont pas r\u00e9p\u00e9t\u00e9es ici pour \u00e9viter de la redondance et pour \u00e9viter de devoir changer cette documentation si les versions ou les d\u00e9pendances \u00e0 installer changent.","title":"Installation"},{"location":"ManuelUtilisateur.html#a-quoi-sert-lapp","text":"Track Trends est une application Windows qui sert d'outil pour des abonn\u00e9s de la F1TV qui veulent exploiter le potentiel de la page DATA de la F1TV. L'application est faite pour un seul cas d'utilisation : R\u00e9cup\u00e9rer les infos d'un Grand Prix en cours ou d'une rediffusion et les afficher de mani\u00e8re plus int\u00e9ressante (en mettant en valeur des infos, en construisant un historique et en en interpr\u00e9tant d'autres). L'application est faite pour que l'utilisateur ait le moins de choses \u00e0 faire pour que tout fonctionne. Cela ne veut pas dire que l'utilisateur n'a rien \u00e0 faire et le but de ce document est d'expliquer les diff\u00e9rentes \u00e9tapes n\u00e9cessaires \u00e0 l'utilisation de l'application.","title":"A quoi sert l'app ?"},{"location":"ManuelUtilisateur.html#utilisation-simple","text":"Le but de cette section est de vous permettre d'apprendre \u00e0 lancer, stopper et modifier les variables de fonctionnement de l'application. Vous serez donc en mesure d'utiliser le programme correctement. Vous devez commencer par lancer le programme. \"Page de lancement\" Au lancement de l'app, vous devriez avoir une page comme ci-dessus. Il y a beaucoup d'infos, mais pour le moment ce qui nous int\u00e9resse, c'est le menu en haut \u00e0 gauche. \"Contr\u00f4les\" Si c'est la premi\u00e8re fois que vous utilisez l'application, il est possible qu'il n'y ait qu'un seul preset au lieu des quatre ci-dessous voire 0 si vous n'avez pas mis le preset inclus dans l'installation. Si vous n'avez aucun preset, passez directement \u00e0 l'\u00e9tape calibration avant de revenir ici. Comme vous pouvez le voir, en plus des presets s\u00e9lectionnables, il y a quatre boutons (dont deux gris\u00e9s) et une boite de texte avec un lien. Le lien est la chose la plus simple. C'est le lien du Grand Prix que vous voulez analyser.","title":"Utilisation \"Simple\""},{"location":"ManuelUtilisateur.html#recuperer-le-lien-vers-le-flux","text":"Pour remplir la boite de texte pr\u00e9cit\u00e9e, il nous faut un lien et ce lien, on le trouve sur la F1TV . Quand vous vous rendez sur la F1TV la page principale devrait ressembler \u00e0 quelque chose comme \u00e7a (\u00e9videmment le contenu ne sera pas le m\u00eame) : \"Page principale de la F1TV\" Si vous ne vous \u00eates pas d\u00e9j\u00e0 connect\u00e9, connectez-vous et ensuite, allez sur la page du Grand Prix qui vous int\u00e9resse. Pour un Grand Prix en direct, normalement, il y a un endroit o\u00f9 vous pouvez cliquer pour acc\u00e9der au flux directement, et si vous voulez utiliser un Grand Prix en diff\u00e9r\u00e9, vous pouvez aller dans \"202x Season\" ou x est l'ann\u00e9e en cours, en l'occurrence 2023. Et l\u00e0, vous pourrez s\u00e9lectionner un Grand Prix. Dans tous les cas, quand vous avez s\u00e9lectionn\u00e9 votre Grand Prix, vous devriez vous retrouver sur une page de ce style : \"Page d'un Grand Prix sur la F1TV\" Et ici si vous \u00eates sur un Grand Prix en direct, vous pouvez directement copier l'URL de la page et si c'est un Grand Prix en diff\u00e9r\u00e9, vous pouvez directement naviguer avec la barre de progression vid\u00e9o du lecteur de la F1TV, Je conseille de la placer juste apr\u00e8s le d\u00e9part. Vous pouvez prendre le lien avant ou apr\u00e8s avoir chang\u00e9 le timecode de la vid\u00e9o, cela ne change rien, le lien ne contient pas votre progression dans la vid\u00e9o. Ensuite, vous pourrez juste mettre le lien que vous avez r\u00e9cup\u00e9r\u00e9 dans la boite de texte que l'on a vu pr\u00e9c\u00e9demment. Il faut simplement s'assurer que le lien contient bien le terme ?action=play \u00e0 la fin.","title":"R\u00e9cup\u00e9rer le lien vers le flux"},{"location":"ManuelUtilisateur.html#boutons","text":"Vous n'avez acc\u00e8s qu'\u00e0 deux boutons au lancement de l'application. Le premier nomm\u00e9 'Config' et le second 'Launch' Le bouton 'Config' permet d'ouvrir la page de calibration (voir section Calibration). Le bouton Launch permet de lancer le programme. Le texte peut changer (Par exemple en 'Retry' si le lancement a rat\u00e9) mais son utilit\u00e9 reste la m\u00eame : Lancer le navigateur virtuel qui sera requis pour traiter les donn\u00e9es. Si vous cliquez sur Launch tous les boutons devraient \u00eatre gris\u00e9s, c'est normal. Pendant le lancement du navigateur, l'application ne peut pas \u00eatre utilis\u00e9e pour \u00e9viter des conflits. Il faut juste attendre. Vous aurez \u00e9galement des fen\u00eatres d'invite de commande qui vont s'ouvrir et se fermer, c'est tout \u00e0 fait normal et il ne faut pas interagir avec tant que tout ne s'est pas stabilis\u00e9. Vous devriez voir enfin une fen\u00eatre comme celle ci : \"Page suspecte (Pas de panique, elle ne l'est pas)\" Il ne faut pas fermer cette fen\u00eatre. C'est la seule chose qui nous relie au navigateur virtuel qu'utilise l'application. Il ne faut pas la fermer, par contre, vous pouvez la r\u00e9duire et la laisser en background, c'est d'ailleurs ce que je vous recommande de faire. Peu apr\u00e8s l'ouverture de cette fen\u00eatre, vous devriez avoir acc\u00e8s \u00e0 trois boutons. Bouton 'Config' qui fait la m\u00eame chose qu'avant Bouton 'Re Launch' qui remplace le bouton 'Launch' qui permet de relancer le processus dans le cas ou vous auriez envie de changer de lien ou de preset. Bouton 'Start' lance le processus de r\u00e9cup\u00e9ration de donn\u00e9es. Si vous cliquez sur ce dernier, vous lancerez le programme. Cela peut prendre quelque secondes \u00e0 s'actualiser, mais tr\u00e8s vite vous devriez vous trouver devant une page comme celle-ci : \"D\u00e9but du programme\" Vous n'aurez alors qu'un seul bouton disponible : le bouton 'Stop'. Vous pouvez arr\u00eater le programme quand vous le voulez. ATTENTION : Avant de fermer le programme, merci de stopper d'abord le processus. Si vous stoppez le processus, vous devriez vous retrouver dans la m\u00eame situation que juste apr\u00e8s avoir lanc\u00e9 le navigateur virtuel : \"Arr\u00eat du programme\" Vous pouvez voir en dessous la derni\u00e8re image que le programme a d\u00e9cod\u00e9e et vous avez de nouveau acc\u00e8s aux trois boutons expliqu\u00e9s pr\u00e9c\u00e9demment. Quand l'application a \u00e9t\u00e9 lanc\u00e9e, vous pouvez aussi cliquer sur les donn\u00e9es affich\u00e9es, mais ces comportements sont expliqu\u00e9s dans la section \"Comprendre les donn\u00e9es\" Voil\u00e0 ! Vous savez comment lancer, stopper et modifier les variables de fonctionnement de l'application. Maintenant, vous pouvez passer directement \u00e0 la section \"Comprendre les donn\u00e9es\" si vous voulez apprendre comment utiliser ce que propose le programme ou passer \u00e0 la section \"Calibration\" si vous voulez apprendre \u00e0 configurer l'application et cr\u00e9er des Presets.","title":"Boutons"},{"location":"ManuelUtilisateur.html#calibration","text":"Ici, vous apprendrez \u00e0 configurer l'application pour cr\u00e9er vos propres presets et surtout pourquoi vous auriez besoin de le faire et \u00e0 quoi servent ces fameux \"Preset\". Vous serez en mesure d'adapter le fonctionnement de l'application, qu'elle soit conforme aux diff\u00e9rents changements d'interface et de pilotes pr\u00e9sents.","title":"Calibration"},{"location":"ManuelUtilisateur.html#pourquoi-calibrer-lapplication-et-creer-un-preset","text":"Le programme que vous allez utiliser utilise de l'OCR (Optical Character Recognition). Cela veut dire que le programme va aller regarder chaque image de la F1TV et convertir ce qu'il y a marqu\u00e9 dessus en donn\u00e9es que l'on peut r\u00e9cup\u00e9rer automatiquement. Sauf que pour y arriver, le programme a besoin de savoir o\u00f9 se trouve le texte important et surtout quel type de donn\u00e9e, il est cens\u00e9 trouver \u00e0 tel ou tel endroit. Pour \u00eatre plus pr\u00e9cis, il a \u00e9galement besoin de savoir quels pilotes peuvent se trouver dans la course actuelle. Et pour qu'il sache tout cela, il y a des fichiers de configuration nomm\u00e9s \"Presets\" qui doivent contenir toutes ces informations. Et pour les cr\u00e9er, il existe la page de calibration qui va demander \u00e0 un humain de lui fournir les infos qu'il ne peut pas trouver par lui-m\u00eame. Normalement, il devrait y avoir un Preset fournis par l'application quand vous la t\u00e9l\u00e9chargez depuis le repo mais vous pouvez avoir besoin de faire votre propre \"Preset\" dans deux cas : Quand les pilotes pr\u00e9sents ne sont pas les m\u00eames (Ex : Si un pilote doit \u00eatre remplac\u00e9 ou si de nouveaux pilotes sont arriv\u00e9s ou bien que d'autres aient pris leur retraite) Quand vous voulez utiliser des Grand Prix fait d'une autre ann\u00e9e que celle pour laquelle le \"Preset\" original a \u00e9t\u00e9 pr\u00e9vu. (L'ann\u00e9e devrait \u00eatre mentionn\u00e9e dans le nom du preset) Il est d\u00e9conseill\u00e9 d'utiliser ce projet pour des Grand Prix couru avant 2022, car l'interface de la page Data de la F1TV peut avoir trop chang\u00e9, mais vous pouvez toujours essayer pour voir en faisant votre propre Preset pour des ann\u00e9es comme 2021 ou 2020.","title":"Pourquoi calibrer l'application et cr\u00e9er un Preset ?"},{"location":"ManuelUtilisateur.html#comment-creer-son-preset","text":"Pour cr\u00e9er son Preset, il faut utiliser la page Settings ou Config de l'application. Pour y acc\u00e9der, il faut simplement cliquer sur le bouton \"Config\" de la page principale et vous serez accueilli par une page qui devrait ressembler \u00e0 celle ci dessous : \"Page de config vide\" La seule diff\u00e9rence est que vous aurez s\u00fbrement moins de Presets dans la liste des Presets voir, vous pouvez n'en avoir aucuns. Comme vous pouvez le voir, vous n'avez pas \u00e9norm\u00e9ment d'options disponibles quand vous lancez simplement l'application. Vous n'avez acc\u00e8s qu'aux boutons \"Start the browser\" et \"Add\",\"Remove\". On verra \u00e0 quoi servent ces deux derni\u00e8res plus tard, mais dans un premier temps, on va se pencher sur le bouton Start. Vous pouvez voir qu'en plus de ce bouton, vous pouvez aussi rentrer du texte dans la boite de texte \"Grand Prix URL\" ce qui n'est pas un hasard. Le but, c'est de mettre le lien vers un Grand Prix pour pouvoir avoir une image de test. Pour voir comment r\u00e9cup\u00e9rer cet URL, voir la section (Utilisation \"simple\" \u2192 \"R\u00e9cup\u00e9rer le lien vers le flux\") Quand l'URL a \u00e9t\u00e9 plac\u00e9e au bon endroit, on peut cliquer sur le bouton \"Start\" et attendre. Si vous n'avez pas encore lu la partie \"Utilisation \"simple\"\", vous allez voir des invites de commandes s'ouvrir et se fermer et c'est tout \u00e0 fait normal. Il faut attendre d'avoir une invite de commande qui reste ouverte et qui ressemble \u00e0 celle ci dessous avant de faire quoi que ce soit. Ensuite, vous pourrez simplement r\u00e9duire cette fen\u00eatre et retourner sur la page de configuration. \"Invite de commande suspecte, mais normale\" Ensuite, si vous attendez un peu (le processus peut prendre jusqu'\u00e0 plusieurs dizaines de secondes), vous aurez soit une erreur, soit vous vous retrouverez avec une page qui ressemble \u00e0 \u00e7a : \"Page de config amorc\u00e9e\" Et l\u00e0 on voit que toutes les options ont \u00e9t\u00e9 d\u00e9bloqu\u00e9es. Avant de se pencher sur les nouvelles options, on va rester sur le fonctionnement g\u00e9n\u00e9ral. Le bouton \"Get a newer image\" permet d'avoir une image plus r\u00e9cente de la F1TV. C'est utile par exemple, car comme on le voit sur l'exemple, l'interface de la F1TV n'a pas encore eu le temps de disparaitre. Mais cela peut aussi \u00eatre utile plus tard dans le cas o\u00f9 vous voudriez que certaines infos s'affichent. Le bouton \"Reset Emulator\", lui, permet, comme son nom l'indique, de relancer le navigateur virtuel que l'on a lanc\u00e9 avec le bouton \"Start the browser\". Cela peut \u00eatre utile dans le cas ou l'image ne soit pas l'image que l'on attendait ou si on veut tenter le coup avec un autre URL. Maintenant, on peut passer \u00e0 la cr\u00e9ation des zones et des windows","title":"Comment cr\u00e9er son Preset"},{"location":"ManuelUtilisateur.html#creation-des-zones-et-des-windows","text":"Sans trop rentrer dans les d\u00e9tails, l'application a besoin de connaitre la localisation de certains \u00e9l\u00e9ments. Elle a besoin de savoir : O\u00f9 se trouve la zone g\u00e9n\u00e9rale des infos pilotes O\u00f9 se trouve chaque pilote O\u00f9 se trouvent les informations pour chacuns de ces pilotes Le programme de calibration est fait pour que vous n'ayez besoin de donner que les informations qu'il ne peut pas deviner. Il y a deux \u00e9tapes : La premi\u00e8re \u00e9tape est de donner les dimensions de la zone principale d'informations. Pour ce faire, il faut dans un premier temps cliquer sur le bouton \"Create the main zone\" qui devrait afficher le texte suivant en dessous apr\u00e8s avoir cliqu\u00e9 : \"Texte indiquant le nombre de points qu'il reste \u00e0 ajouter\" Ensuite, il va falloir cliquer directement sur l'image pour indiquer le coin en haut \u00e0 gauche et le point en bas \u00e0 droite de la zone rectangulaire qui contient les informations. Vous pouvez voir en bleu ci-dessous les coins du rectangle et en rouge l'endroit o\u00f9 il faut cliquer. \"Infographie expliquant o\u00f9 placer les points de la zone principale\" Il faut faire attention \u00e0 bien prendre tous les pilotes MAIS il faut \u00e9galement faire attention \u00e0 ne pas prendre le texte alentours. \"Texte qui ne doit pas \u00eatre dans la zone s\u00e9lectionn\u00e9e\" Ci dessus, on peut voir du texte barr\u00e9 en violet. Il ne faut surtout pas que la zone vienne inclure ces bouts de texte ou toute la calibration pourrait rater. Ensuite, si vous avez bien fait votre travail, au deuxi\u00e8me clic sur la page, vous aurez quelques secondes de flottement et ensuite, vous devriez avoir les contours que vous avez dessin\u00e9s affich\u00e9s en jaune avec pleins de plus petites zones \u00e0 l'int\u00e9rieur comme ci-dessous : \"Zone principale avec les zones de pilotes automatiquement calcul\u00e9es\" Le programme a non seulement pris en compte la zone, mais il a aussi d\u00e9tect\u00e9 automatiquement o\u00f9 se trouvaient les zones de chaque pilote. Si vous n'avez pas un r\u00e9sultat comme celui-l\u00e0 et/ou que les zones ne sont pas bien align\u00e9es sur les pilotes sur l'image, je vous conseille de r\u00e9essayer de cr\u00e9er la zone principale. La seconde \u00e9tape est de montrer o\u00f9 sont les fen\u00eatres d'infos au programme. Vous aurez peut-\u00eatre remarqu\u00e9 qu'en dessous de l'image principale, quand l'affichage jaune a \u00e9t\u00e9 appliqu\u00e9, une image est apparue. Elle devrait ressembler \u00e0 quelque chose dans ce style : \"Image d'une zone de pilote\" C'est une zone de pilote de l'image que l'on voit au-dessus et c'est ici que l'on va indiquer les zones int\u00e9ressantes. Pour ce faire, il faut cliquer sur le bouton \"Create Windows\" et ce message devrait s'afficher un peu en dessous : \"Texte indiquant le nombre de windows \u00e0 ajouter\" Cela nous indique le nombre de fen\u00eatres qu'il nous reste \u00e0 s\u00e9lectionner. La technique est la m\u00eame que pour la grande zone sauf que l\u00e0, il faut le faire neuf fois. Et il faut absolument le faire dans l'ordre de gauche \u00e0 droite. Le but est de tout s\u00e9lectionner et d'arriver \u00e0 ce r\u00e9sultat : \"Exemple de fen\u00eatres d\u00e9coup\u00e9es\" Dans l'ordre, de gauche \u00e0 droite, on veut r\u00e9cup\u00e9rer : La position du pilote Son \u00e9cart avec le leader (en l'occurrence comme c'est le leader que l'on voit, on doit s\u00e9lectionner la fen\u00eatre ou il est marqu\u00e9 \"LEADER\") Son dernier temps au tour La fen\u00eatre DRS L'\u00e9tat de ses pneus Son nom Son temps au secteur 1 Son temps au secteur 2 Son temps au secteur 3","title":"Cr\u00e9ation des Zones et des Windows"},{"location":"ManuelUtilisateur.html#ajout-du-nom-des-pilotes","text":"La derni\u00e8re info qui manque \u00e0 notre programme est la liste des pilotes pr\u00e9sents. On peut interagir avec cette liste ici : \"Menu d'interaction avec la liste de pilotes\" Dans la boite de texte, on peut \u00e9crire le nom d'un pilote qui peut \u00eatre trouv\u00e9 sur l'image, on peut l'ajouter \u00e0 la liste et si on a fait une erreur, on peut le retirer. Et c'est \u00e0 peu pr\u00e8s tout. Il ne reste maintenant plus qu'\u00e0 mettre tous les noms. TIP : Il peut \u00eatre int\u00e9ressant d'ajouter le nom des pilotes de r\u00e9serve pour \u00e9viter de venir changer son preset si un pilote n'est pas pr\u00e9sent pour cause de maladie ou de blessure. Il faut simplement faire attention de ne pas non plus mettre trop de noms pour \u00e9viter que le programme puisse confondre. Et voil\u00e0 ! On a toutes les infos n\u00e9cessaires. La page de configuration devrait ressembler \u00e0 \u00e7a : \"Image de la page de config apr\u00e8s avoir ajout\u00e9 toutes les infos\" Il ne reste plus qu'\u00e0 le sauvegarder.","title":"Ajout du nom des pilotes"},{"location":"ManuelUtilisateur.html#sauvegarder-le-nouveau-preset","text":"Pour sauvegarder le preset, on peut facilement lui donner un nom dans la zone de texte sous les trois boutons de contr\u00f4le des Presets et cliquer sur Save current Preset. Et voil\u00e0, vous savez d\u00e9sormais comment cr\u00e9er vos propres presets et \u00e0 quoi ils servent. Vous pouvez donc adapter le fonctionnement du projet pour qu'il soit conforme aux changements de pilotes et d'interface avec les ann\u00e9es. Vous pouvez ainsi utiliser l'application normalement en s\u00e9lectionnant votre nouveau preset \u00e0 chaque fois.","title":"Sauvegarder le nouveau Preset"},{"location":"ManuelUtilisateur.html#load-un-preset-existant","text":"Le loading est un peu sp\u00e9cial. Il faut d\u00e9j\u00e0 avoir lanc\u00e9 le navigateur virtuel pour activer les boutons. Ensuite, il suffit de s\u00e9lectionner un preset et de cliquer sur \"Load the Preset\" et attendre un petit peu. L'affichage sera un peu bizarre, mais c'est normal, il ne faut pas s'inqui\u00e9ter si la zone de pilote affich\u00e9e en dessous est correcte. \"Exemple de ce \u00e0 quoi peut ressembler un loading et l'affichage bizarre que \u00e7a implique\" \u00c0 partir de l\u00e0, il est facile de faire son propre preset en changeant juste les noms des pilotes par exemple.","title":"Load un Preset existant"},{"location":"ManuelUtilisateur.html#comprendre-les-donnees","text":"Ici, vous allez apprendre \u00e0 utiliser l'application et ses donn\u00e9es en comprenant \u00e0 quoi elles servent et comment elles sont affich\u00e9es. \"Image de l'application en cours de fonctionnement depuis quelques minutes\" Il y a 5 types de donn\u00e9es :","title":"Comprendre les donn\u00e9es"},{"location":"ManuelUtilisateur.html#overtakes","text":"Cette fen\u00eatre est loin d'\u00eatre compl\u00e8tement op\u00e9rationnelle, mais elle permet de voir l'historique des d\u00e9passements et des changements de position. Il faut scroller pour voir les plus r\u00e9cents. Je dirais que pour le moment, c'est la moins int\u00e9ressante et elle ne fonctionne pas toujours super.","title":"Overtakes"},{"location":"ManuelUtilisateur.html#last-five-laps","text":"\"Image de la fen\u00eatre des cinq derniers tours\" En total contraste avec la fen\u00eatre Overtakes, ici, on a peut-\u00eatre la fen\u00eatre la plus int\u00e9ressante de toutes. On peut voir les pilotes actuellement les plus rapides et plus lents sur le circuit. Le calcul est fait sur la moyenne des cinq derniers tours de chaque pilote. Cela permet de se faire une id\u00e9e de la situation des pilotes. Cela peut servir par exemple \u00e0 d\u00e9tecter quand un pneu est plus rapide que les autres quand on voit des pilotes dans le milieu ou bas de tableau appara\u00eetre dans le plus rapide. Il est par exemple int\u00e9ressant de voir la diff\u00e9rence de vitesse entre les pneus secs et pluie sur un circuit qui commence \u00e0 s\u00e9cher. On peut progressivement voir les pilotes en pneus secs devenir de plus en plus rapides alors que ce sont des pilotes beaucoup moins rapide en temps normal. On peut cliquer sur n'importe lequel de ces pilotes pour voir ses infos appara\u00eetre dans la fen\u00eatre Driver Infos pour voir les cinq derniers tours par exemple ou les pneus qu'il est en train de chausser.","title":"Last Five Laps"},{"location":"ManuelUtilisateur.html#battles","text":"\"Fen\u00eatre des batailles\" Cette fen\u00eatre est \u00e9galement assez int\u00e9ressante, car elle permet de voir les pilotes qui sont en train de se battre. Il est estim\u00e9 qu'un pilote qui se bat est un pilote qui est \u00e0 trois secondes ou moins du pilote devant lui. Ne sont affich\u00e9es que les batailles de quatre pilotes maximums dans l'ordre du classement. Si un pilote est entre 2 et 3 secondes de son adversaire, l'\u00e9cart est en blanc. Entre 1 et 2 secondes, il est \u00e9crit en jaune. Dans la zone du DRS (Une seconde ou moins) il est \u00e9crit en vert. On peut aussi cliquer sur le nom d'un pilote pour afficher ses infos dans la fen\u00eatre des infos pilote. Cette fen\u00eatre permet de mieux comprendre qui sont les pilotes qu'il faut garder \u00e0 l'\u0153il \u00e0 la TV ou simplement qui sont les pilotes qui sont tr\u00e8s proches et qui pourraient se mettre la pression pour les arr\u00eats aux stands, car jamais un pilote ne reste \u00e0 moins de trois secondes d'un autre si \u00e7a n'est pas pour tenter quelque chose au niveau strat\u00e9gique.","title":"Battles"},{"location":"ManuelUtilisateur.html#driver-infos","text":"\"Fen\u00eatre des infos pilote\" Ici, on peut voir toutes les infos lives d'un pilote. Rien de fou \u00e0 dire sur la partie de gauche, par contre la partie de droite est un peu plus int\u00e9ressante, car elle contient un historique de ses cinq derniers tours (dans la photo, il n'y en a qu'un seul, mais au fur et \u00e0 mesure de la course cela se remplit). On peut non seulement voir les cinq derniers temps au tour, mais on peut aussi cliquer sur chacun d'eux pour voir les secteurs associ\u00e9s. \"Exemple d'affichage des secteurs d'un temps au tour\" ;","title":"Driver infos"},{"location":"ManuelUtilisateur.html#live-ranking","text":"\"Fen\u00eatre du classement en direct\" Ceci est la fen\u00eatre la plus simple. C'est tout b\u00eatement le classement actuel avec les \u00e9carts avec le leader. Note : On peut \u00e9galement cliquer sur les diff\u00e9rents pilotes pour en afficher les infos dans la fen\u00eatre infos pilote.","title":"Live Ranking"},{"location":"ManuelUtilisateur.html#erreurs","text":"Il est tr\u00e8s probable que si vous utilisez beaucoup cette application, vous allez rencontrer des erreurs. Ici, vous pourrez apprendre ce qu'elles veulent dire et ce que vous pouvez faire pour y rem\u00e9dier. Il n'existe pas un tr\u00e8s grand nombre d'erreurs, mais voici les principales Erreur 100 (Souvent au premier d\u00e9marrage) Cette erreur signale un probl\u00e8me avec la r\u00e9cup\u00e9ration de cookies. Cela peut \u00eatre caus\u00e9 par une mauvaise installation de python ou si vous ne vous \u00eates pas connect\u00e9s r\u00e9cemment \u00e0 la F1TV depuis Chrome ou que vous n'avez tout simplement pas install\u00e9 Chrome sur votre machine. Erreur 101 (Moins r\u00e9current qu'\u00e0 une \u00e9poque) Cette erreur veut dire qu'il y a d\u00e9j\u00e0 une instance de navigateur ouverte. Pour r\u00e9gler cette erreur, regarder dans votre barre des t\u00e2ches les invites de commande ouvertes et fermez celui qui correspond \u00e0 un ancien navigateur. (Si c'est d\u00e9j\u00e0 fait alors en derniers recours, vous pouvez chercher dans le gestionnaire des t\u00e2ches et chercher \"GeckoDriver.exe\") Erreur 102 Cela peut \u00eatre une erreur qui arrive, car vous n'avez pas donn\u00e9 un URL valide pour la F1TV ou par ce que vous ne vous \u00eates pas connect\u00e9 r\u00e9cemment \u00e0 la F1TV depuis chrome (Si vous veniez de le faire alors, attendez un peu et r\u00e9essayez). Parfois cela peut prendre un peu de temps \u00e0 s'actualiser). Erreur 103 L'URL est invalide Erreur 104 L'URL est invalide Erreur 105 Cette erreur indique que soit vous avez donn\u00e9 un URL qui ne correspond \u00e0 aucun Grand Prix, soit que la vid\u00e9o a mis trop de temps \u00e0 charger. Vous pouvez essayer de vous brancher en Ethernet ou simplement r\u00e9essayer si vous \u00eates s\u00fbr de votre URL. Erreur 106 M\u00eame chose que pour la 105","title":"Erreurs"},{"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 }, e t c... } ] } } ] } } 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\" , e t c... ] } 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 ( 0 x50 , 0 x50 , 0 x50 ); 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\" Je pense que cette version est meilleure m\u00eame si elle est encore plus en bordel par ce que le texte permet de se faire une meilleure id\u00e9e de l'utilis\u00e9 de chaque partie. Mercredi 10 Mai 2023 Bon hier je n'ai pas eu le temps de finir la documentation de la recup\u00e8ration d'images et de la calibration. Il faudra donc que je repasse un coup dessus en fin de semaine je pense. Mais la j'aimerais avancer sur la mise en commun du projet, comme la configuration fonctionne plut\u00f4t pas mal je pense que je vais juste vite fait aller commenter les methodes qui ne le sont pas encore et ensuite je vais passer \u00e0 l'impl\u00e9mentation de l'OCR. Je suis presque certain que l'OCR va avoir besoin de plus de r\u00e8glages mais bon on verra bien. Je me rend compte en commentant que la methode de load serait plus efficace avec un tout petit peu plus d'infos de la part du JSON. J'aurais pu ajouter l'offset entre chaque Driver Zone pour eviter un l\u00e8ger drift lors de la reconstruction. Mais bon rien de grave donc je pense que je vais le laisser comme ca pour le moment \u00e0 moins que ca me pose soucis plus tard. J'ai eu quelques soucis avec les images en 4K. Du coup j'ai descendu les variables d'environnement \u00e0 1920x1080 En fait il y a parfois un soucis un peu p\u00e9nible avec l'OCR. Parfois pour un temps comme ci dessous: \"1:45.140\" Le programme ne va pas bien comprendre les ponctuations et il va donner : 1115140 La il y a deux probl\u00e8mes... Le 1:xx.xxx est compris comme 11xxxxx et le 4 s'est transform\u00e9 en 1... J'ai cr\u00e9\u00e9 ce \"petit\" bout de code pour g\u00e8rer les fois ou les '.' et les ':' ont mal \u00e9t\u00e9 interpr\u00eat\u00e9s if ( rawNumbers . Count == 1 ) { //If this code is used it means that its bad ... //The methods that comes are really not that great and are juste quick fixes try { result = Convert . ToInt32 ( rawNumbers [ 0 ]); switch ( windowType ) { case OcrImage . WindowType . Sector : //The usual sector is in this form : 33.456 if ( rawNumbers [ 0 ]. Length == 6 ) { //The '.' has been understood like a number result = 0 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ] + rawNumbers [ 0 ][ 1 ]) * 1000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 3 ] + rawNumbers [ 0 ][ 4 ] + rawNumbers [ 0 ][ 5 ]); } if ( rawNumbers [ 0 ]. Length == 5 ) { //The '.' has been overlooked result = 0 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ] + rawNumbers [ 0 ][ 1 ]) * 1000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ] + rawNumbers [ 0 ][ 3 ] + rawNumbers [ 0 ][ 4 ]); } break ; case OcrImage . WindowType . LapTime : //The usual Lap time is in this form : 1:45:345 if ( rawNumbers [ 0 ]. Length == 6 ) { //The '.' and ':' have been overlooked //I Know Im skipping the cases where there are more than 9 minuts but it happens so rarely that... we dont care result = 0 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]) * 60000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 1 ] + rawNumbers [ 0 ][ 2 ]) * 1000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 3 ] + rawNumbers [ 0 ][ 4 ] + rawNumbers [ 0 ][ 5 ]); } if ( rawNumbers [ 0 ]. Length == 7 ) { //There is two possibilities //Either 1:45.140 has been interpreted as 1145.10 or 1:451140. We will assume its the first one result = 0 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]) * 60000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ] + rawNumbers [ 0 ][ 3 ]) * 1000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 4 ] + rawNumbers [ 0 ][ 5 ] + rawNumbers [ 0 ][ 6 ]); } break ; case OcrImage . WindowType . Gap : //The usual Gap is in this form : + 34.567 if ( rawNumbers [ 0 ]. Length == 5 ) { //The '.' has been overlooked result += Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ] + rawNumbers [ 0 ][ 1 ]) * 1000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ] + rawNumbers [ 0 ][ 3 ] + rawNumbers [ 0 ][ 4 ]); } break ; } if ( rawNumbers [ 0 ]. Length > 6 ) { //The number definitely has been interpreted wrong } } catch { //It can be because the input is empty or because its the LEADER bracket result = 0 ; } } else { //Auuuugh result = 0 ; } ConfigFile = \"./Presets/Clean_2023.json\" ; string gpUrl = \"https://f1tv.formula1.com/detail/1000006688/2023-azerbaijan-grand-prix?action=play\" ; Bon je n'arrive pas \u00e0 faire fonctionner l'OCR sans tout faire crash \u00e0 chaque fois. Je vais abandonner le travail de la journ\u00e9e pour revenir au point initial... C'est tr\u00e8s frustrant mais bon je ne vois pas comment faire mieux. Rien ne marche alors qu'avant ca marchant super sur le projet OCR normal. Va savoir pourquoi m\u00eame comme ca, impossible de faire marcher l'OCR. Il y a un soucis au niveau de l'ASYNC qui me fait crash tout le temps en me disant qu'un objet est deja en train d'\u00eatre utilis\u00e9. Ca marchait nikel dans mes premi\u00e8res version je ne vois pas pourquoi ca p\u00eate maintenant. Je pense que je vois \u00e0 peu pr\u00e8s le soucis. public virtual async Task < DriverData > Decode ( List < string > driverList ) { int sectorCount = 0 ; DriverData result = new DriverData (); Parallel . ForEach ( Windows , async w => { // A switch would be prettier but I dont think its supported in this C# version if ( w is DriverNameWindow ) result . Name = ( string ) await ( w as DriverNameWindow ). DecodePng ( driverList ); if ( w is DriverDrsWindow ) result . DRS = ( bool ) await ( w as DriverDrsWindow ). DecodePng (); if ( w is DriverGapToLeaderWindow ) result . GapToLeader = ( int ) await ( w as DriverGapToLeaderWindow ). DecodePng (); if ( w is DriverLapTimeWindow ) result . LapTime = ( int ) await ( w as DriverLapTimeWindow ). DecodePng (); if ( w is DriverPositionWindow ) result . Position = ( int ) await ( w as DriverPositionWindow ). DecodePng (); if ( w is DriverSectorWindow ) { sectorCount ++; if ( sectorCount == 1 ) result . Sector1 = ( int ) await ( w as DriverSectorWindow ). DecodePng (); if ( sectorCount == 2 ) result . Sector2 = ( int ) await ( w as DriverSectorWindow ). DecodePng (); if ( sectorCount == 3 ) result . Sector3 = ( int ) await ( w as DriverSectorWindow ). DecodePng (); } if ( w is DriverTyresWindow ) result . CurrentTyre = ( Tyre ) await ( w as DriverTyresWindow ). DecodePng (); }); return result ; } Ca c'est ma methode de decoding de chaque Driver Zone. Le message d'erreur me parle d'une windowImage quand il dit qu'un objet est d\u00e9ja utilis\u00e9. Ma conjecture c'est que en essayant de faire toutes les windows en m\u00eame temps. Elles veulent parfois acc\u00e8der \u00e0 l'image principale en m\u00eame temps. Ce qui evidemment pose probl\u00e8me. Je pense que le fix le plus simple serait de faire le traitement sans le parallele quitte \u00e0 exporter ce fonctionnement sur chaque zone en elle m\u00eame pour ne pas perdre trop de performances. Ok je crois que je vois ou est le soucis. En fait dans cette version du programme c'est toujours la premi\u00e8re image qui \u00e9tait juste tout le temps prise et dans la premi\u00e8re image on a une partie des chiffres qui est bloqu\u00e9e par l'UI de la fen\u00eatre... lol... EN FAIT J'avais un soucis dans ma gestion des chiffres mal faits. Visiblement parfois quand je ne prenais pas en compte un :, un LapTime etait compris comme un Gap to leader ou un Secteur Bon j'en ai tellement marre... Je n'arrive tout simplement PAS \u00e0 faire fonctionner l'OCR ca crash tout le temps j'en peux plus. J'ai tent\u00e9 de r\u00e8gler les probl\u00e8mes de mauvaises detections de secteurs et temps au tour qui font crasher l'app : if ( rawNumbers . Count == 2 ) { //ss:ms result = ( Convert . ToInt32 ( rawNumbers [ 0 ]) * 1000 ) + Convert . ToInt32 ( rawNumbers [ 1 ]); if ( result > ( 60000 + 999 )) { if ( windowType == OcrImage . WindowType . LapTime ) { result = 0 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]) * 60000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString ()) * 1000 ; result += Convert . ToInt32 ( rawNumbers [ 1 ]); } if ( windowType == OcrImage . WindowType . Sector ) { int seconds = 0 ; if ( rawNumbers [ 0 ]. Length == 3 ) { //We have one char that we need to delete //For no apparent reason im going to delete the first seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 1 ]. ToString () + rawNumbers [ 0 ][ 2 ]. ToString ()); } else { seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString () + rawNumbers [ 0 ][ 1 ]. ToString ()); } int ms = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString () + rawNumbers [ 0 ][ 1 ]. ToString () + rawNumbers [ 0 ][ 2 ]. ToString ()); result = seconds * 1000 + ms ; } } } Mais toujours impossible de faire fonctionner cette M**** C'est juste infernal. Je pense que je vais encore tout retirer et remplacer par ce que j'ai dans mon projet OCR original. Donc c'est une journ\u00e9e de perdue compl\u00eatement... C'est extr\u00eamement frustrant. Apr\u00e8s des heures de debug j'ai enfin r\u00e9ussi \u00e0 faire fonctionner le programme de temps en temps. Mais j'ai toujours le soucis que l'image ne veut pas changer alors que je fais tout pour et que l'OCR est nulle \u00e0 chier du coup... Jeudi 11 Mai 2023 Bon apr\u00e8s une bonne nuit de sommeil je vais reprendre les choses depuis le d\u00e9but. J'ai deux soucis : L'OCR pue du derche L'Image que l'on d\u00e9code ne change pas Pour la premi\u00e8re partie j'ai ma petite th\u00e9orie. Je pense que comme je donne des images 4K alors que le feed est en 1080P, il y a d\u00e9ja un genre d'interpolation qui est faite. Je pense donc qu'il faut que j'adapte mon engine pour qu'il fonctionne avec cette r\u00e9solution. Je me suis demand\u00e9 si ca n'\u00e9tait pas mieux de prendre en compte les deux r\u00e9solutions pour les pc un peu moins bal\u00e8zes et j'ai d\u00e9cid\u00e9 de n'en avoir rien a faire. On verra dans le futur si c'est une feature que je voudrais ajouter mais c'est en dehors du scope du dipl\u00f4me je pense. Pour la seconde partie, je pense qu'il faut que j'aille voir du c\u00f4te de OCR_Decode et de OCR Tester pour voir comment je faisais. Je dois forc\u00e9ment oublier un truc. Bon ca commence mal, quand je vais voir dans le projet OCR_Decode, le changement d'image est exactement le m\u00eame et il fonctionne alors que de mon c\u00f4t\u00e9 ca n'est pas le cas. Alors deux choses. Je me rend compte que le changement d'images n'a AUCUN effet sur la detection de texte, et seconde chose, le d\u00e9calage est trop grand entre les windows. Des que le soucis d'image est r\u00e8gl\u00e9 il va falloir que je change drastiquement ma facon de stocker la config en JSON. Il faut que je conserve les \u00e9carts. Sinon regardez ce que ca donne quand on arrive au dernier pilote : \"Zone de pilote d\u00e9cal\u00e9e\" Je commence \u00e0 devenir FOU. Je n'arrive pas \u00e0 changer cette foutue image wtf... J'ai beau tenter par tous les moyens de la changer par une image noire, l'image semble toujours rester celle du d\u00e9part. Bon j'ai enfin trouv\u00e9 pourquoi et je n'ai pas envie de dire comment j'ai trouv\u00e9... Je pense que l'on a tous droit \u00e0 son petit jardin secret. Maintenant ca veut dire que je peux me focus sur le concept important qui est le changement de la cr\u00e9ation et de la lecture des JSON. Voici un exemple de preset JSON : { \"Main\" : { \"x\" : 40 , \"y\" : 355 , \"width\" : 3784 , \"height\" : 1438 , \"Zones\" : [ { \"DriverZone\" : { \"x\" : 0 , \"y\" : -10 , \"width\" : 3784 , \"height\" : 71 , \"Windows\" : [ { \"Position\" : { \"x\" : 47 , \"y\" : 11 , \"width\" : 72 }, \"GapToLeader\" : { \"x\" : 445 , \"y\" : 13 , \"width\" : 201 }, \"LapTime\" : { \"x\" : 859 , \"y\" : 14 , \"width\" : 221 }, \"DRS\" : { \"x\" : 1094 , \"y\" : 13 , \"width\" : 173 }, \"Tyres\" : { \"x\" : 1270 , \"y\" : 11 , \"width\" : 1452 }, \"Name\" : { \"x\" : 2727 , \"y\" : 11 , \"width\" : 351 }, \"Sector1\" : { \"x\" : 3083 , \"y\" : 10 , \"width\" : 253 }, \"Sector2\" : { \"x\" : 3339 , \"y\" : 14 , \"width\" : 195 }, \"Sector3\" : { \"x\" : 3518 , \"y\" : 14 , \"width\" : 250 } } ] } } ] }, \"Drivers\" : [ \"Perez\" , \"Leclerc\" , \"Sainz\" , \"Alonso\" , \"Stroll\" , \"Russel\" , \"Verstappen\" , \"Zhou\" , \"Ocon\" , \"Hulkenberg\" , \"Hamilton\" , \"Norris\" , \"Tsunoda\" , \"Magnussen\" , \"Piastri\" , \"Albon\" , \"Gasly\" , \"Sargeant\" , \"Bottas\" , \"De Vries\" ] } Je pense que ce qui serait bien ce serait de rajouter un \"offsets\" qui contienne les 19 \u00e9carts restants. Bon... la structure de ma fabrication de JSON etait trop confuse je trouve alors je l'ai compl\u00eatement refaite. J'ai aussi abandonn\u00e9 l'id\u00e9e de faire un fichier le plus petit possible car au final on s'en fiche et le plus important c'est que toutes les windows et les zones soient aux bons endroits. Ca nous fait un fichier d'environs 1300 lignes mais au moins le code pour la serialisation est plut\u00f4t clean : public void SaveToJson ( List < string > drivers , string configName ) { string JSON = \"\" ; JsonObject jsonFileObject = new JsonObject (); //Creating the mainZone object JsonObject mainZoneObject = new JsonObject (); mainZoneObject . Add ( \"x\" , MainZone . Bounds . X ); mainZoneObject . Add ( \"y\" , MainZone . Bounds . Y ); mainZoneObject . Add ( \"width\" , MainZone . Bounds . Width ); mainZoneObject . Add ( \"height\" , MainZone . Bounds . Height ); JsonArray driverZonesArray = new JsonArray (); int DriverID = 0 ; foreach ( Zone driverZone in MainZone . Zones ) { DriverID ++; JsonObject driverZoneObject = new JsonObject (); driverZoneObject . Add ( \"name\" , \"Driver\" + DriverID ); driverZoneObject . Add ( \"x\" , driverZone . Bounds . X ); driverZoneObject . Add ( \"y\" , driverZone . Bounds . Y ); driverZoneObject . Add ( \"width\" , driverZone . Bounds . Width ); driverZoneObject . Add ( \"height\" , driverZone . Bounds . Height ); JsonArray windowsArray = new JsonArray (); JsonObject windowObject = new JsonObject (); foreach ( Window window in driverZone . Windows ) { windowObject . Add ( window . Name , new JsonObject { { \"x\" , window . Bounds . X }, { \"y\" , window . Bounds . Y }, { \"width\" , window . Bounds . Width }, { \"height\" , window . Bounds . Height } }); } windowsArray . Add ( windowObject ); driverZoneObject . Add ( \"Windows\" , windowsArray ); driverZonesArray . Add ( driverZoneObject ); } mainZoneObject . Add ( \"DriverZones\" , driverZonesArray ); JsonArray driversArray = new JsonArray (); foreach ( string driver in drivers ) { driversArray . Add ( driver ); } mainZoneObject . Add ( \"Drivers\" , driversArray ); jsonFileObject . Add ( \"Main\" , mainZoneObject ); JSON = jsonFileObject . ToString (); //Saving the file string path = CONFIGS_FOLDER_NAME + configName ; if ( File . Exists ( path + \".json\" )) { //We need to create a new name int count = 2 ; while ( File . Exists ( path + \"_\" + count + \".json\" )) { count ++; } path += \"_\" + count + \".json\" ; } else { path += \".json\" ; } File . WriteAllText ( path , JSON ); } Et normalement la lecture devrait \u00eatre encore plus simple. En fait c'\u00e9tait pas beaucoup plus simple mais au moins maintenant ca marche. Je vais pas mettre le code de lecture ici car c'est un peu trop long donc il va falloir me croire sur parole. (Ou aller sur Git) Bon bah on est au m\u00eame endroit qu'hier... Bon pour demain le plan de bataille ca va \u00eatre : Changer compl\u00eatement la methode \"GetTimeFromPng\" pour qu'elle prenne en compte toutes les possibilit\u00e9s de bugs et d'oubli de '.' ou de ':' mais pas selon le nombre de blocs mais selon le type de temps que l'on cherche Pour le moment je regarde le nombre de blocs et si il y en a deux alors c'est que c'est un temps de secteur. En fait non cela peut aussi \u00eatre un temps au tour qui a rat\u00e9 un point. Il faut que je bosse juste un peu vite fait la dessus et que j'arr\u00eate de putain de crasher d\u00e8s que un truc est pas au bon format. Ensuite quand ca aura arr\u00eat\u00e9 de crasher je vais reprendre l'OCR et voir pourquoi les resultats sont nuls a chier comme ca. Et le but c'est que demain soir j'ai une reconnaissance de caract\u00e8res plus proche de ce que j'avais dans d'autres projets... J'y croit 0 mais bon l'espoir fait vivre comme on dit. vendredi 12 Mai 2023 Bon aujourd'hui il faut que ca marche. On va y aller par \u00e9tape. Je vais revoir toutes les methodes d'OCR et essayer de r\u00e9apliquer les filtres differemment et revenir au point de d\u00e9part. Avant de commencer je note plusieurs soucis avec les premiers tests : Les positions des pilotes ont l'air pas mal (Pas besoin de tout changer mais peut-\u00eatre simplement checker que les filtres sont bons) L'\u00e9cart avec le leader est \u00e9tonnamment pas mal aussi Le temps au tour est tout simplement horrible. Aucun n'est juste et de tr\u00e8s loin m\u00eame si les d\u00e9cimales ne sont pas forc\u00e9ment loin Le DRS je n'ai pas eu l'occasion de bien le tester mais je dirais que ca devrait \u00eatre bon (\u00e0 verifier quand le reste sera bon) Les pneus ne sont \u00e9tonnamment pas SI horribles, m\u00eame si parfois les lettres sont prises comme des chiffres Les noms de pilotes sont tr\u00e8s bon (pas \u00e9tonnant vu le syst\u00e8me de distance de Levenstein donc ca m\u00e9rite quand m\u00eame un petit check) Les secteurs sont en g\u00e9n\u00e9ral horribles mais pas toujours. C'est peut-\u00eatre un soucis de d\u00e9cimale ou des 4 qui se transforment en 1 J'ai remarqu\u00e9 que les 4 sont souvent pris comme des 1. Peut-\u00eatre que en ayant des images 4K l'interpolation est un peu diff\u00e9rente de ce que j'ai l'habitude de voir. Mais donc le plan aujourd'hui c'est de checker tous ces points et les faire fonctionner (youpi...) J'ai d\u00e9sactiv\u00e9 toutes les methodes de cette facon : int sectorCount = 0 ; DriverData result = new DriverData (); foreach ( Window w in Windows ) { // 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); result . Name = \"Unknown\" ; if ( w is DriverDrsWindow ) //result.DRS = (bool)await (w as DriverDrsWindow).DecodePng(); result . DRS = false ; if ( w is DriverGapToLeaderWindow ) //result.GapToLeader = (int)await (w as DriverGapToLeaderWindow).DecodePng(); result . GapToLeader = 0 ; if ( w is DriverLapTimeWindow ) //result.LapTime = (int)await (w as DriverLapTimeWindow).DecodePng(); result . LapTime = 0 ; if ( w is DriverPositionWindow ) //result.Position = (int)await (w as DriverPositionWindow).DecodePng(); result . Position = 0 ; if ( w is DriverSectorWindow ) { sectorCount ++; if ( sectorCount == 1 ) //result.Sector1 = (int)await (w as DriverSectorWindow).DecodePng(); result . Sector1 = 0 ; if ( sectorCount == 2 ) //result.Sector2 = (int)await (w as DriverSectorWindow).DecodePng(); result . Sector2 = 0 ; if ( sectorCount == 3 ) //result.Sector3 = (int)await (w as DriverSectorWindow).DecodePng(); result . Sector3 = 0 ; } if ( w is DriverTyresWindow ) //result.CurrentTyre = (Tyre)await (w as DriverTyresWindow).DecodePng(); result . CurrentTyre = new Tyre ( Tyre . Type . Undefined , 0 ); } return result ; Le but c'est que ensuite je puisse y aller \u00e9tape par \u00e9tape. Position : Alors pour cette reconnaissance je dirais que la 4K fait des merveilles qui permettent de retirer du processing. La position apr\u00e8s un simple Treshold est assez bien reconnue et la dilataion et/ou Erosion ne sont pas nescessaires finalement. On va donc pouvoir gagner un certain temps et c'est un bon signe pour la suite. On peut aussi noter que quand un pilote est hors course toutes ses valeurs sont gris\u00e9es et sa position est prise comme un -1 Sectors, alors j'ai refait toute la partie qui concerne les secteurs et qui les nettoie. Et je me suis rendu compte qu'ils \u00e9taient bien souvent juste, le seul truc c'est que ils s'affichent de mani\u00e8re un peu sp\u00e9ciale. En fait dans la page de la F1TV les secteurs peuvent faire plus de 60 secondes sans passer sur un affichage de minutes. Ce qui fait que c'est un peu bizarre \u00e0 regarder mais c'est parfaitement juste. J'ai aussi pu simplifier la reconnaissance gr\u00e2ce \u00e0 l'image de meilleure qualit\u00e9 et maintenant les temps de secteur sont plut\u00f4t corrects. En fait le plus long et complexe c'est de pr\u00e9voir les cas particuliers ou un '.' a \u00e9t\u00e9 mal interpr\u00eat\u00e9... Et en parlant de ca, je vais aller m'occuper des temps au tour qui ajoutent une couche de complexit\u00e9 avec un '.' ET un ':' qui peuvent \u00eatre oubli\u00e9s... Oh et j'y pense, un truc malin pourrait \u00eatre de comparer les temps au tour et les temps de secteur. Ils devraient concorder normalement je pense. Ok je viens de finir la gestion des temps au tour... J'ai un code de genre 170 lignes pour juste nettoyer le resultat dans le cas ou des '.' n'ont pas \u00e9t\u00e9 trouv\u00e9s ou des ':' ont \u00e9t\u00e9 n'ont pas \u00e9t\u00e9 trouv\u00e9 ou si l'un des deux s'est transform\u00e9 en chiffre etc.. etc.. etc.. Le soucis c'est que la maintenant je me rend compte que les \u00e9carts entre les pilotes vont juste \u00eatre un enfer \u00e0 nettoyer... Ils peuvent aussi bien \u00eatre \"0.760\" comme \"1:34.456\" du coup... je sais pas vraiment comment faire pour tout nettoyer. Je pense que je vais juste en avoir rien \u00e0 faire et tant pis si de temps en temps c'est pas g\u00e9nial. Bon du coup j'ai pas pris en compte TOUS les cas possibles mais d\u00e9ja un certain nombre et c'est d\u00e9ja pas mal. Sur les diff\u00e9rents Grand Prix d'exemples ca a l'air de plut\u00f4t bien tourner ! Mais ca demande tellement de tests et de code que c'est un peu ridicule... La methode \"GetTimeFromPng\" fait d\u00e9ja presque 430 lignes \u00e0 cause de tous les cas possibles et tous les try catch. Ca peut para\u00eetre peu \u00e9l\u00e9gant mais j'ai essay\u00e9 de mettre des commentaires un peu partout pour permettre \u00e0 nimporte qui de comprendre ce qui se passe. J'ai aussi pu faire les pneus et maintenant (roulement de tambour) Ca marche (presque) En fait j'ai des soucis parfois quand les pneus sont un peu cach\u00e9s dans les permiers tours : Pneus cach\u00e9s Le soucis c'est que du coup le chiffre est un peu illisible... mais je pense que avec l'historique il devrait y avoir moyen de ne pas prendre en compte les chiffres de pneus pendant cinq tours apr\u00e8s le changement de pneu ou de simplement tenter de faire les calculs de pneus. Pour conclure la journ\u00e9e je pense que je devrais avoir le temps de faire un syst\u00e8me qui permet de refresh \u00e0 volont\u00e9. Par contre je viens de d\u00e9couvrir que quand un temps de secteur est en couleur on arrive pas \u00e0 le lire. Ah et la detection prend un peu moins de trois secondes sur mon pc je crois. Mais c'est seulement si les driver zones sont faites en m\u00eame temps mais en faisant ca de temps en temps ca crash et \u00e0 chaques fois c'est d'un endroit diff\u00e9rent du coup je comprend pas vraiment. Sinon ca prend dix secondes. Lundi 15 Mai 2023 Aujourd'hui c'est journ\u00e9e poster et visites. Comme on va avoir des visites de premi\u00e8res ann\u00e9es voire de terminales et que le soir c'est visite des parents. Je pense que j'ai finit de tout regrouper (\u00e0 part \u00e9videmment le traitement et le stockage des donn\u00e9es) ce qui veut dire que je suis pas dans une superbe posture. Il va falloir que je sois tr\u00e8s efficace dans la partie stockage de donn\u00e9es et mise en place du mod\u00e8le si je veux avoir une chance de rendre un joli travail de dipl\u00f4me. (et m\u00eame comme ca je peux voir que le temps commence \u00e0 manquer) Dans l'id\u00e9al je devrais avoir termin\u00e9 la partie stockage jeudi... Ce qui veut dire que je n'ai que trois jours pour le faire et que en plus jeudi je dois travailler depuis la maison. Ca va pas \u00eatre simple. Mais aujourd'hui je vais m'occuper d'adapter la documentation de l'OCR et faire la documentation de toute la partie r\u00e9cup\u00e8ration d'images et de la calibration. Bon au final la journ\u00e9e a \u00e9t\u00e9 un peu difficile. On a pas vraiment pu travailler l'apr\u00e8s midi car il a fallu pr\u00e9senter le projet environs 10 fois \u00e0 toutes les classes et \u00e0 des parents voire futurs experts. Les d\u00e9mos ont plut\u00f4t bien fonctionn\u00e9es j'en suis assez content. Mais ca veut dire que la partie doc a pas forc\u00e9ment pu \u00eatre totalement compl\u00eat\u00e9e mais demain il va falloir que je m'occupe de la suite du projet. Mardi 16 Mai 2023 Bon aujourd'hui c'est la partie stockage qui doit \u00eatre faite. Il y a plusieurs solutions possibles \u00e0 ce probl\u00e8me. Mais comme je n'ai besoin que d'une base de donn\u00e9e locale et que je ne veux pas que chaque utilisateur doive installer un serveur sur sa machine je pense que je vais utiliser une base de donn\u00e9es SQLITE. Il y a eu une petit intervention de mr Bonvin qui est venu me donner une id\u00e9e pour la partie OCR. En fait j'avais un soucis quand je voulais d\u00e9coder du texte de couleur. M\u00eame en appliquant un filtre de gris je n'arrivais pas \u00e0 faire reconnaitre les chiffres. Et il m'a dit que une bonne id\u00e9e cela pourrait de prendre la valeur max de chaque channel et de la faire appliquer \u00e0 tous ce qui blanchit assez bien l'image. J'ai d\u00e9cid\u00e9 d'exag\u00e8rer le blanchiment et cela donne des r\u00e9sultats plut\u00f4t... int\u00e9ressants... \"Filtre vanish oxy action sur un secteur violet\" Le soucis c'est que le violet est une couleur quand m\u00eame assez sombre alors il va falloir que je fasse un syst\u00e8me de treshold un peu sp\u00e9cial qui soit un peu plus sympa et qui prenne plus facilement des couleurs plus basses. Une methode \u00e0 laquelle j'ai pens\u00e9 pour detecter dans quel tour chaque pilote est serait de garder en m\u00e9moire toutes les infos de chaques pilotes au fur et \u00e0 mesure, et d\u00e8s qu'on ne recoit plus d'infos des secteurs ou que le temps au tour a chang\u00e9 on peut savoir qu'il faut passer au tour suivant. Pour detecter les arr\u00eats aux stands je peux essayer de detecter un changement de type de pneus ou de nombre de tours detect\u00e9s sur le m\u00eame pneu Au d\u00e9part je me disais que je pourrais peut-\u00eatre faire une base de donn\u00e9e SQLITE locale qui puisse \u00eatre reprise d'un Grand Prix \u00e0 un autre. Mais je me suis dit que de faire des statistiques inter Grand Prix \u00e9tait un peu en dehors du scope du projet. La base de donn\u00e9e sera donc cr\u00e9\u00e9e \u00e0 chaque d\u00e9marrage de l'app La mani\u00e8re dont je vois les choses en ce moment est qu'on aie deux sources de donn\u00e9es dans l'affichage final. On aurait une partie des infos qui seraient en direct depuis la detection : Les ecarts entre pilotes La position des pilotes Le dernier temps au tour Les derniers secteurs Les pneus Mais on aurait aussi des rubriques cr\u00e9\u00e9es de toutes pi\u00e8ces par des infos qui viennent de la BD Voici les rubriques qui pourraient \u00eatre int\u00e9ressantes \u00e0 voir dans l'interface finale : Les 3 ou 5 pilotes les plus rapides ces cinq derniers tours Le pilote qui a le plus fait de d\u00e9passements Les batailles en cours Les 3 pilotes les plus lents Un classement pond\u00e9r\u00e9 avec les 20s de moins pour tous les pilotes qui ne se sont pas encore arr\u00eat\u00e9s En gros l'id\u00e9e serait que on update une fois par tour et par pilote la base de donn\u00e9e avec des infos comme le temps au tour, le type de pneu etc... Voici les trois tables que je vais cr\u00e9er : Drivers Colonne Type de Data Description Tag ID INTEGER ID du pilote PRIMARY Name VARCHAR Nom du pilote NOT NULL Pitstops Colonne Type de Data Description Tag Lap INTEGER Tour durant lequel le Pitstop a \u00e9t\u00e9 effectu\u00e9 PRIMARY DriverID INTEGER Pilote qui a effectu\u00e9 le Pitstop PRIMARY Tyre VARCHAR Pneu chauss\u00e9 par le pilote NOT NULL Stats Colonne Type de Data Description Tag Lap INTEGER Tour durant lequel le Pitstop a \u00e9t\u00e9 effectu\u00e9 PRIMARY DriverID INTEGER Pilote qui concern\u00e9 PRIMARY Tyre VARCHAR Pneu chauss\u00e9 par le pilote NOT NULL LapTime INTEGER Temps au tour (MS) NOT NULL Sector1 INTEGER Temps du secteur 1 (MS) NOT NULL Sector2 INTEGER Temps du secteur 2 (MS) NOT NULL Sector3 INTEGER Temps du secteur 3 (MS) NOT NULL GapToLeader INTEGER Ecart avec le leader (MS) NOT NULL Position INTEGER Position pilote NOT NULL Ca n'est pas forc\u00e9ment d\u00e9finitif mais je pense que c'est d\u00e9ja un bon d\u00e9but pour faire des rubriques sympa. Je suis en train de tenter d'impl\u00e9menter le code pour permettre ensuite d'ajouter et retirer des choses facilement. Mercredi 17 Mai 2023 Aujourd'hui le but c'est de remplir la base SQLITE avec des infos. Si j'arrive \u00e0 tout remplir alors ca devrait pas \u00eatre trop compliqu\u00e9 de venir faire des requ\u00e8tes qui donnent de bonnes infos. Mais la probl\u00e9matique principale va \u00eatre de d\u00e9cider QUAND ins\u00e8rer des choses dans la base de donn\u00e9e. Je pense que le meilleur moyen serait de garder une liste de DriverData par pilote en piste qui puisse contenir toutes les data que l'on recoit. Et \u00e0 chaques fois que l'on veut ajouter \u00e0 cette liste on v\u00e9rifie si un tour a \u00e9t\u00e9 fait pour envoyer les data pr\u00e9c\u00e9dente et r\u00e9initialiser la liste. Il faut donc une liste de 20 listes de DriverData et une liste de int qui repr\u00e9senteront le num\u00e9ro du tour dans lequel chaque pilote se trouve. Pour detecter un arr\u00eat je pense que la meilleure mani\u00e8re est de regarder si le pilote a chang\u00e9 de place ou de type de pneu. Si je prend que les fois ou le pilote change place ET de pneus alors certains arr\u00eats pour ceux qui sont loins devant ou loins derri\u00e8re pourraient ne pas \u00eatre detect\u00e9s. Et si je ne prend que le changement de pneus cela pose un soucis car un pneu pourrait avoir \u00e9t\u00e9 chang\u00e9 pour un autre du m\u00eame type. Et parfois les valeurs de tours faits avec le pneu ne sont pas toujours bien lues et parfois sont compliqu\u00e9es \u00e0 retrouver car tous les pneus ne sont pas neuf quand ils sont chauss\u00e9s. Je crois que la detection de tours et des arr\u00eats aux stands est sur la bonne voie. Le seul soucis que j'ai c'est que pour faire du debug je suis un peu oblig\u00e9 d'attendre pendant 10min si je veux avoir de quoi faire des stats un peu sympa. Je me rend compte que parfois j'obtiens des r\u00e9sultats un peu bizarres mais que c'est la f1TV qui les donne. Par exemple de cas ou Alex Albon n'a que deux tours sur ses pneus alors que tout le monde devant et derri\u00e8re lui en a 3 et que on est au tour 4. \"wtf...?\" Ah et aussi parfois quand les pilotes se d\u00e9passent on se retrouve dans des situations plut\u00f4t rigolotes : \"Wtf...??\" COMMENT JE FAIS POUR DETECTER CA WIONDVIDNJDODVNSDIC Bon je me rend compte que clairement si je veux que mes data soient plus utiles il faudrait que je fasse un tout petit peu plus de taff sur quelques points dans l'OCR. Le nombre de tours des pneus (Les num\u00e9ros sont vraiment mal detect\u00e9s et parfois m\u00eame la couleur est pas dingue) Les 4 qui sont pris pour des 1 ou des 11 (pour les temps et les pneus) apr\u00e8s un test de plus longue haleine je suis content de voir que au moins mon programme peut tourner plus d'une heure sans crasher et qu'il peut \u00eatre fiable quand il veut. Jeudi 18 Mai 2023 Aujourd'hui c'est t\u00e9l\u00e9Travail forc\u00e9 et j'\u00e9tais scens\u00e9 aller au Grand Prix d'Imola ce qui malheureusement ne pourra pas se faire pour des raisons d'inondations. En effet la r\u00e9gion est clairement pas en \u00e9tat de recevoir un Grand Prix de Formule 1 et donc ce weekend c'est maison. Le but du jour c'est d'avancer la doc et de tenter d'am\u00e9liorer l'OCR pour que Lundi il soit relativement facile d'avancer sur l'interface de l'app finale. Je suis en train d'explorer une methode de detection de bords de sobel. Le seul soucis c'est que les r\u00e9sultats sont bons mais avec un vide au milieu des chiffres. Cela veut dire que parfois le temps est mal detect\u00e9. Mais il semble que pour le reste du temps cela se passe plut\u00f4t bien. Ca vaut peut \u00eatre le coup de modifier la gestion des erreurs. \"Artefacts de detection de sobel\" En fait le soucis avec ces artefacts c'est que parfois le temps au tour n'est tout simplement pas detect\u00e9. Dans l'exemple ci dessus, la reconnaissance de caract\u00e8res ne trouve tout simplement rien. Il faut donc que je trouve un moyen de corriger ces soucis. Il semblerait que en appliquant un tresholding un peu plus s\u00e9v\u00e8re en amont on arrive \u00e0 r\u00e9duire les artefacts \"Artefacts all\u00e8g\u00e9s\" ; Je crois qu'il faut faire attention avec les 'Bitmap.save' quand on utilise de l'asynchrone. le GDI+ aime pas des masses. Lundi 22 Mai 2023 BON ! Il ne reste plus beaucoup de temps ! Selon le planning cette semaine est la derni\u00e8re semaine de programmation. Il va donc falloir CHARBONNER !! Il ne me reste plus que une t\u00e2che \u00e0 vraiment faire (\u00e0 part les tests mais euuuu voila bon). J'essaie d'impl\u00e9menter un peu plus d'error handling mais c'est pas facile... Il y a tellement de choses qui peuvent mal tourner c'est infernal. Une chose qui serait bien serait de rajouter des points d'attente variables dans le code de l'emulateur un peu partout pour eviter de se retrouver bloqu\u00e9 \u00e0 chaque fois. L'int\u00e9r\u00eat serait que des gens avec une moins bonne connexion pourraient quand m\u00eame profiter du programme sans qu'il crashe 300 fois. Ce qui est frustrant c'est que va savoir pourquoi, maintenant, on arrive quasi jamais a avoir la page data... Genre sans deconner c'est une fois sur 5 que l'emulateur nous ressort la page Data et pas juste le feed. C'est absolument infernal. Je ne comprends pas pourquoi en plus. L'emulateur arrive bien \u00e0 cliquer sur le bon bouton mais m\u00eame comme ca ca ne veut pas. CA NA AUCUN SENS BORDEL. Sur une image Jjai de supers resultats pour les temps au tour mais d\u00e8s que l'image change PAF plus aucun temps n'est detect\u00e9. Ah non c'est bon c'est juste que VA SAVOIR POURQUOI les images sont en putain de resolution DEGEULASSE. Je ne comprends pas pourquoi ce matin particuli\u00e8rement le projet marche si mal. Par ce que d\u00e8s que l'image revient \u00e0 une r\u00e9solution normale c'est bon. Un autre soucis que j'ai est que je n'arrive pas \u00e0 paralleliser l'OCR ce qui fait que elle peut prendre parfois plus de 15 secondes. Et le probl\u00e8me avec ca c'est que la detection de tours et de pitstop est grandement impact\u00e9e si on a pas assez de data assez souvent. Je vais me focus sur le reste en attendant mais d\u00e8s que M.Bonvin apparait dans les parages je vais devoir l'alpaguer. J'ai ajout\u00e9 la possibilit\u00e9 d'essayer plusieurs fois de trouver le bouton fullscreen et de cliquer dessus plut\u00f4t que d'attendre dix secondes comme un con et esp\u00e8rer que ca fonctionne. Mais si apr\u00e8s 15 secondes d'essais il n'y arrive pas cela fait quand m\u00eame p\u00eater une erreur. Je pense que je vais m'occuper de la page de configuration maintenant. Voici \u00e0 quoi ressemblait la page de settings ce matin quand je suis arriv\u00e9. \"Ancienne page de settings\" Comme je pense que l'UI de cette page ne va pas vraiment changer d'ici la fin du projet je peux me permettre de lui faire une petite beaut\u00e9 car apr\u00e8s je ne pense pas y retoucher. Pour ca j'ai plusieurs \u00e9tapes comme choisir une palette de couleur, retirer l'inutile et choisir judicieusement le placement des items sur la form pour que cela soit le plus intuitif possible. J'y pense, il fuadrait peut-\u00eatre que je me trouve un logo ca pourrait rendre bien. Voila alors j'ai chang\u00e9 un tout petit peu ce \u00e0 quoi ressemble la page de settings et j'ai ajout\u00e9 du responsive pour que le user puisse mettre l'application en plein \u00e9cran. \"Nouvelle page de configuration\" Mais il manque un peu de couleurs et de d\u00e9tails pour que cela rende vraiment bien. Et apr\u00e8s quelques tentatives on se retrouve avec une page plut\u00f4t sympa je trouve : \"Nouvelle page de config\" Et elle est responsive : \"Nouvelle page de config en plein \u00e9cran\" Et j'ai fait quelques changements pour ce qui est des zones qui s'affichent pour qu'on les voie mieux. \"Nouvelle page de config en action\" Je trouve que franchement ca rend pas mal. Le reste de l'app sera dans ce style. J'ai mis pas mal de temps \u00e0 cr\u00e9er cette page, mais je pense que c'est important que la page de config soit propre. Et en plus tout le temps que j'ai pass\u00e9 ici n'est pas perdu car ensuite j'aurai simplement \u00e0 suivre les m\u00eames directives de style pour le reste de l'UI. Il faut aussi savoir que Windows Form n'est ps forc\u00e9ment le meilleur outil pour travailler avec le design. Truc tout b\u00eate par exemple qui m'a fait perdre 30 minutes. Il est impossible de retirer les bordures des objets \"GroupBox\". Ce que j'ai donc du faire ca a \u00e9t\u00e9 de dessiner un rectangle autour de la couleur du background pour que l'on ne voit plus les bordures et ensuite j'ai du redessiner le texte pour qu'il puisse s'afficher quand m\u00eame. C'est pleins de petites choses comme ca qui sont plut\u00f4t p\u00e9nibles et qui font perdre du temps mais je pense que c'est rentable de s'y attarder. Maintenant ce que je vais faire aujourd'hui et demain c'est l'affichage general de l'app. Je pense que je vais commencer par mettre des placeholder de l'app finale comme ca je saurai quoi implementer comme methodes de r\u00e9cup\u00e8ration demain. J'aimerais quand m\u00eame faire une interface sympa m\u00eame si les data sont pas parfaites. Par ce que je me dis que au pire si je montre une interface qui donne des infos inexacte mais qui a la bonne logique c'est toujours mieux que de ne pas montrer ce que ca pourrait faire avec des donn\u00e9es un peu plus int\u00e8gres. Autre point \u00e0 noter, je me suis rendu compte que ca pourrait \u00eatre potentiellement pas mal de trouver un moyen rapide de lancer l'appli avec un Grand Prix. Genre permettre de selectionner le preset et l'URL du Grand Prix sans avoir \u00e0 passer par la page de configuration. Je me suis rendu compte que c'\u00e9tait super chiant de devoir \u00e0 chaque fois le faire (m\u00eame si je me rends compte que normalement un user ne devrait pas lancer l'app autant) Voila ue premi\u00e8re version de l'App avec tous les placeholders : \"Premi\u00e8re version de la page principale\" Et c'est tout pour aujourd'hui ! Ce fut une journ\u00e9e remplie. Mardi 23 Mai 2023 Aujourd'hui le but c'est de remplir le framework de hier avec les bonnes Data. Je ne sais pas si je peux tout finir en un jour mais on va essayer. Bon j'ai eu une discussion anim\u00e9e avec M.Bonvin et il semble que je sois oblig\u00e9 de refaire \u00e0 peu pr\u00e8s tout mon code pour le rendre ne serait-ce qu'un peu optimis\u00e9. Bon au final j'ai perdu 6H de travail \u00e0 tenter de convertir mon code dans une version un peu plus optimis\u00e9e... Mais je me rend compte que c'est juste impossible... Il me faudrait au moins plusieurs jours pour faire correctement ce refactor et donc je vais tout simplement faire un git restore... C'est extr\u00eament frustrant mais bon... Pas le choix il semble. J'ai du \u00e9crire au moins 600 lignes de code et tout pars en fum\u00e9e. C'est une d\u00e9bauche d'\u00e9nergie absolument ph\u00e9nomenale. Apr\u00e8s ca valait le coup de tenter je pense. (J'ai envie de mourir) EN FAIT C'EST BON !! Il fallait juste que je croie en mon code original !!! J'ai r\u00e9ussi \u00e0 paralelliser mon ancien code. Il ne manquait presque rien mais M.Bonvin voulait absolument que je change le reste. Maintenant j'ai une detection qui se fait en quelques secondes c'est genial. Mercredi 24 Mai 2023 Alors hier je n'ai pas bien eu le temps d'expliquer ce que voulait que M.Bonvin. En fait mon programme actuellement utilise un d\u00e9coupage qui peut par\u00e2itre complexe. Et de par sa nature, il pensait qu'il \u00e9tait simplement impossible de paralelliser le traitement car trop complexe et trop couteux. Il voulait donc que je passe sur un traitement plus simple. L'id\u00e9e \u00e9tait que on s'occupe dabord de faire une liste de toutes les Windows et de les traiter toutes \u00e0 la fois pour \u00e9viter que les boucles soient trop complexes. Sauf que pour impl\u00e9menter un truc pareil c'est \u00e9norm\u00e9ment de code car cela va \u00e0 l'encontre totale de la facon dont mon projet fonctionne actuellement. Mais comme j'\u00e9tais ouvert \u00e0 d'autres solutions. J'ai pass\u00e9 six heures \u00e0 tenter de l'impl\u00e9menter. Il en aurait fallu au minimum deux jours soyons clair. Et en fait on s'est retrouv\u00e9s devant pleins de probl\u00e8mes qui ne se posent pas dans mon architecture originale. Par exemple. On a pas trouv\u00e9 de methode simple pour d\u00e9couper les images des fen\u00eatres de mani\u00e8re thread safe. Il aurait donc fallu ajouter des boucles en pr\u00e9alable pour tout d\u00e9couper et le faire de mani\u00e8re s\u00e9quencielle. Ensuite vient le probl\u00e8me que si on traite toutes les donn\u00e9es dans des boucless paralelle on perds leur position originale donc il faut faire une classe pour stocker les r\u00e9sultats temporaires. Il y a aussi le soucis que les Windows ont certe une position mais elle est relative au parent et \u00e0 l'image parente. Donc il faudra faire un syst\u00e8me qui convertis les windows en position absolue sur l'image. Ca peut para\u00eetre \u00eatre de simples changements mais deja il y en a pas mal d'autres et franchement m\u00eame si l'id\u00e9e originale aurait pu simplifier les choses. Les sacrifices que l'on doit faire pour la faire marcher sont juste trop moches et \u00e0 mon avis ne sont pas du tout aussi logiques que mon d\u00e9coupage original. Cette exp\u00e9rience m'a quand m\u00eame permis de me rendre compte des endroits dans mon code qui sont plus ou moins difficile \u00e0 maintenir et cela m'a fait me rendre compte que ma solution n'\u00e9tait pas forc\u00e9ment la plus simple pour tout le monde mais que mine de rien elle peut \u00eatre efficace. La je suis en train de rajouter les routes pour la vue. Je me suis dit que ca serait une bonne id\u00e9e de permettre aux users de cliquer sur un pilote pour avoir ses infos. Mais je me suis dit que ce qui serait encore plus cool serait de pouvoir cliquer sur un des temps au tour d'un pilote et qu'une petite fen\u00eatre s'ouvre pour indiquer les temps par secteurs. Mais en faisant ca je me rends compte qu'il y a quelques soucis dans la facon que je conserve les infos dans la DB et je peux voir directement quand la reconnaissance a du mal avec certains pilotes ou des positions. Ca arrive plus souvent que ce que je voudrais que un pilote soit mal detect\u00e9. Mais ce qui est dr\u00f4le c'est que c'est parfois sur une deux voire trois reconnaissance que le pilote n'est plus reconnu mais ensuite tout va bien. Il faut que je travaille un peu plus sur le filtrage de ces donn\u00e9es limites et peut-\u00eatre de voir si la reconnaissance de la position pourrait \u00eatre un peu v\u00e9rifi\u00e9e. Bon pour \u00eatre honn\u00eate je ne pense pas que le code qui concerne l'affichage soit le meilleur code que j'aie pu produire dans ma vie de d\u00e9veloppeur mais en m\u00eame temps je n'ai pas forc\u00e9ment le temps de le rendre magnifique. La le but est simplement que tout marche. (Et c'est un peu la m\u00eame phylosophie dans tout le reste du projet lol) Demain il me reste pas mal de choses \u00e0 faire et c'est la derni\u00e8re journ\u00e9e ou je peux les faire. Rendre la form plus jolie et changer les couleurs Rendre la form Responsive Ajouter les bons messages d'erreur qui vont bien Modifier les messageBox d'erreur pour qu'elles soient plus agr\u00e9ables \u00e0 utiliser Clean un peu le code mod\u00e8le vue controller Si j'ai le temps ajouter les bons commentaires les bonnes ent\u00eates partout Jeudi 25 Mai 2023 Bon bah le but aujourd'hui c'est de finaliser un peu le projet car la semaine prochaine c'est doc. Pas grand chose \u00e0 dire. J'ai pass\u00e9 la journ\u00e9e \u00e0 fix des petits bugs par ci par la. Voici des exemples de ce \u00e0 quoi ressemble l'app \u00e0 la fin de la journ\u00e9e : \"Screenshot de la page principale\" \"Screenshot de la page principale\" On se rend jamais compte mais c'est tellement long de r\u00e8gler chaque petit soucis un par un. Il y a tellement de possibilit\u00e9s de choses qui peuvent mal tourner ou qui ont un comportement diff\u00e9rent selon l'ordre dans lequel on fait les choses. Mais dans l'ensemble, m\u00eame si on est pas sur la meilleure interface que l'on aie vu dans l'histoire. Je trouve que elle fait quand m\u00eame le taff. Vendredi 26 Mai 2023 Aujourd'hui c'est d\u00e9part pour Monaco mais comme l'avion etait bien en retard j'ai pu avancer sur le nettoyage du code. Grand Prix de Monaco [Insert photos] Lundi 29 Mai 2023 Mon vol pour Geneve hier soit a \u00e9t\u00e9 annul\u00e9 et je dois donc prendre une deviation car tous les vols pour Geneve sont pleins. Je dois partir a 7h30 pour prende l'avion de 9H pour Nantes et de la bas je dois prendre un avion \u00e0 17h pour arriver \u00e0 18h30 \u00e0 l'a\u00e9roport de Geneve. C'est pas pratique car j'avais pr\u00e9vu d'avancer aujourd'hui et je suis oblig\u00e9 d'avancer comme je peux dans l'a\u00e9roport. J'avance encore sur le nettoyage rapide du code. Le but est que demain je puisse sortir la premi\u00e8re release en Beta et que je mette vraiment serieusement \u00e0 la Documentation. M.Jayr m'a \u00e9galement demand\u00e9 de lui donner le document d'\u00e9valuation interm\u00e9diaire. Il ne faut pas que j'oublie demain. J'ai mis des notes sur mon code au cas ou des gens viennent \u00e0 le lire. J'y d\u00e9cris les choses que j'aurais fait diff\u00e9remment ou qui pourraient para\u00eetre bizarre au tout venant. Mardi 30 Mai 2023 Bon aujourd'hui je dois encore avancer sur la partie nettoyage de code et avant de sortir la premi\u00e8re release Beta je vais tenter d'installer le projet sur un autre pc pour voir ce que je n'ai pas mentionn\u00e9 dans mon ReadMe. Bon j'ai pu rendre \u00e0 M.Jayr mon evaluation interm\u00e9diaire et j'ai fait les derni\u00e8res modifications sur le projet pour que je puisse sortir une release correcte. Maintenant je vais me mettre \u00e0 la documentation. Il va falloir que je revoie ce que j'ai d\u00e9ja \u00e9crit en ce qui concerne l'OCR car j'ai fait des modifications depuis et j'ai ajout\u00e9 la methode de SOBEL. Il faut peut-\u00eatre aussi que je parle vite fait dans la partie Emulation que j'ai du changer les variables d'environnement pour faire fonctionner le syst\u00e8me en 4K. Mercredi 31 Mai 2023 Doc Jeudi 1 Juin 2023 Bon je me suis rendu compte que je n'avais fait vraiment aucuns tests et que c'est franchement bof. Je pense que ce que je devrais faire pour faire des tests unitaires c'est prendre des exemples de chaque type de windows possibles en plusieurs exemplaires. Ensuite je note le r\u00e9sultat que j'attends et je regarde si ca me retourne la bonne valeur. Mais ca veut dire que ca va me prendre pas mal de temps de tout mettre en place mais ca m'aurait s\u00fbrement fait gagner beaucoup de temps si je l'avais fait d\u00e8s le d\u00e9but... Je pense que une bonne id\u00e9e serait de prendre trois Grand Prix et de prendre une photo de chaque type de window au d\u00e9but et \u00e0 la fin. Plus je regarde plus je me rend compte que ce pojet aurait carr\u00e9ment du \u00eatre en TDD (Test Driven Developement) par ce que ca m'aurait fait gagner un temps FOU. OK JE SUIS DEBILE POURQUOI J'AI PAS FAIT CA PLUS T\u00d4T ??? En fait ce que j'aurais du faire c'est prendre de gros \u00e9chantillons de toutes les types de windows et j'aurais un parfait framework pour savoir si j'ai am\u00e9lior\u00e9 mon OCR ou non. Voici les exemples que je vais utiliser pour verifier le bon fonctionnement de l'OCR : \"Dataset Ecarts\" ; \"Dataset Tours\" ; \"Dataset Noms\" ; \"Dataset Positions\" ; \"Dataset Sectors\" ; \"Dataset Tyres\" ; Je pense que c'est un set assez correct car j'ai essay\u00e9 de prendre un peu tous les cas possibles. Le seul qui m'inqui\u00e8te un peu c'est celui des pneus mais bon. C'est aussi celui qui m'inqui\u00e8te le plus en temps normal. Non mais c'est juste g\u00e9nial les tests en fait... j'avais pas vu que parfois ma detection de GAP TO LEADER comprenait le \"+1:34.567\" en \"61:34.567\" car le '+' \u00e9tait interpr\u00eat\u00e9 comme un 6. Sans les tests je ne m'en serais pas rendu compte. Ce qui est g\u00e9nial c'est que ca veut dire que si je veux am\u00e9liorer mon OCR j'ai juste \u00e0 mettre plus d'exemples dans le dossier de tests et de run les tests et voir ou il a des soucis. C'est un peu tard mais ca m'aurait fait gagner TELLEMENT de temps c'est absolument ridicule. Vendredi 2 Juin 2023 Aujourd'hui c'est de nouveau doc. Mais la je pense qu'il faut que je rajoute une rubrique \"Fonctionnement g\u00e9n\u00e9ral\" qui r\u00e9sume tr\u00e8s simplement toutes les \u00e9tapes du projet avec un bon diagramme. En fait c'est beaucoup plus dur que ce que je pensais de faire un bon diagramme qui explique tout sans \u00eatre illisible. J'ai mis une heure trente \u00e0 faire celui la : \"Diagramme fonctionnement g\u00e9n\u00e9ral\" J'ai pris pas mal de temps aussi \u00e0 faire ces trois autre diagrammes un peu plus graphiques qui montrent de mani\u00e8re un poil plus abstraite le fonctionnement des trois grosses parties du projet. \"Diagramme recup\u00e9ration d'images\" \"Diagramme OCR\" \"Diagramme Traitement\" Je pense qu'ils sont un peu plus faciles \u00e0 comprendre que le gros diagramme g\u00e9n\u00e9ral. Ce que je me dis c'est que je vais faire une section fonctionnement g\u00e9n\u00e9ral ou je vais expliquer les trois parties avec les petits diagramme et r\u00e9sumer avec le grand diagramme. Lundi 5 Juin 2023 Ce weekend j'ai voulu essayer le projet pour le Grand Prix de Barcelone qui s'est sold\u00e9 en un \u00e9chec cuisant... J'ai eu trois soucis pour l'utilisation du programme. Les voici dans l'ordre croissant d'importance et de difficult\u00e9 \u00e0 r\u00e8gler : Mon Laptop est incapable de rester plus d'une heure allum\u00e9 m\u00eame compl\u00eatement charg\u00e9 \u00e0 100% Le WIFI de chez moi a du mal \u00e0 g\u00e8rer plusieurs flux 1080p en m\u00eame temps et donc le temps de chargement est trop long ce qui me fait des erreurs 105 \"Ce que voyait le programme pendant les erreurs 105 ce dimanche\" Le fait que quand un flux est en live, quand on clique dessus, un nouveau bouton appar\u00e2it qui n'\u00e9tait pas pr\u00e9vu et qui nous propose de regarder depuis le d\u00e9but ou en live. Le soucis avec ce bouton c'est qu'il n'est pr\u00e9sent que pour les sessions live... Il va donc falloir attendre le prochain GP (Canada 18 Juin) pour faire un test en conditions r\u00e9elles. \"Ce que voyait le programme avant que j'essaie d'impl\u00e9menter le click auto\" J'ai essay\u00e9 de r\u00e8gler le probl\u00e8me en Live, le soucis c'est que j'ai un commentaire \u00e0 fournir pendant la course et donc je n'ai pas eu le temps de plus me pencher sur le cas. Le weekend du prochain Grand Prix je vais essayer de tester sur les sc\u00e9ances d'essais libres.le click du bouton et j'amenerai mon cable chez moi. Bon sinon aujourd'hui, comme d'hab, Doc... Je vais faire le manuel utilisateur. J'ai fait un manuel qui d\u00e9crit \u00e0 peu pr\u00e8s tout ce qui'il faut savoir pour bien utiliser l'app. Cela fait un document un peu long mais je pense que c'est nescessaire car c'est vraiment pas une app facile \u00e0 comprendre quand on ne vient pas du milieu de la F1 (et m\u00eame la...) Mardi 6 Juin 2023 Aujourd'hui je vais continuer \u00e0 documenter... Ma methode pour l'instant c'est juste de remplir les titres que j'ai pr\u00e9vu au d\u00e9part. Ensuite \u00e0 partir de jeudi (je pense que c'est \u00e0 partir de cette date que j'aurai un peu tout rempli) j'aimerais bien relire la grille d'\u00e9valuation et ensuite faire une lecture de mon journal de bord pour v\u00e9rifier que je n'ai rien oubli\u00e9. Et le but c'est de finir la doc Vendredi soir pour faire une derni\u00e8re release doc + projet. Un coll\u00e8gue M.Briard m'a pas mal aid\u00e9 avec la configuration de mon mkdocs et il a d\u00e9velopp\u00e9 une extension pour ajouter le code source au PDF final. La il est en train de regarder si il peut trouver un moyen de faire une table des figures qui nous est demand\u00e9e. (j'ai plus de 200 images dans ma documentation alors si c'est possible de ne pas avoir \u00e0 faire une table des figures \u00e0 la main je prends) Mercredi 7 Juin La je suis en train de parler de l'optimisation de mon application et je viens de me rappeller qu'il manquait des methodes avec de la paralellisation alors je vais les convertir avant de continuer la doc. En fait je viens de me rendre compte qu'aucunes de mes methodes de filtres n'\u00e9taient en parralel... Je ne sais pas si jaurai le temps de le faire aujourd'hui en fait Voici le code avant la paralellisation : public static Bitmap VanishOxyAction ( 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 blue = ( int ) pixel [ 0 ]; int green = ( int ) pixel [ 1 ]; int red = ( int ) pixel [ 2 ]; int max = Math . Max ( Math . Max ( blue , green ), red ); if ( max > 255 / 3 ) max = 255 ; pixel [ 0 ] = pixel [ 1 ] = pixel [ 2 ] = ( byte ) max ; } } } inputBitmap . UnlockBits ( bmpData ); return inputBitmap ; } Et voici \u00e0 quoi ca ressemble avec la paralellisation : public Bitmap VanishOxyAction ( Bitmap inputBitmap ) { unsafe { BitmapData bitmapData = inputBitmap . LockBits ( new Rectangle ( 0 , 0 , inputBitmap . Width , inputBitmap . Height ), ImageLockMode . ReadWrite , inputBitmap . PixelFormat ); int bytesPerPixel = System . Drawing . Bitmap . GetPixelFormatSize ( inputBitmap . PixelFormat ) / 8 ; int heightInPixels = bitmapData . Height ; int widthInBytes = bitmapData . Width * bytesPerPixel ; byte * PtrFirstPixel = ( byte *) bitmapData . Scan0 ; Parallel . For ( 0 , heightInPixels , y => { byte * currentLine = PtrFirstPixel + ( y * bitmapData . Stride ); for ( int x = 0 ; x < widthInBytes ; x = x + bytesPerPixel ) { int blue = currentLine [ x ]; int green = currentLine [ x + 1 ]; int red = currentLine [ x + 2 ]; int max = Math . Max ( Math . Max ( blue , green ), red ); if ( max > 255 / 3 ) max = 255 ; currentLine [ x ] = currentLine [ x + 1 ] = currentLine [ x + 2 ] = ( byte ) max ; } }); inputBitmap . UnlockBits ( bitmapData ); } return inputBitmap ; } Les performances n'ont pas beaucoup augment\u00e9 mais au moins comme ca c'est fait Jeudi 8 Juin 2023 Aujourd'hui, le but est de finir la documentation et de mettre les derniers commentaires dans le code et faire une premi\u00e8re release en BETA. Pour ce genre de travail un peu p\u00e9nible je conseille une bonne playlist de phonk. Ca permet d'\u00e9crire en rythme.","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 }, e t c... } ] } } ] } } 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\" , e t c... ] } 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 ( 0 x50 , 0 x50 , 0 x50 ); 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\" Je pense que cette version est meilleure m\u00eame si elle est encore plus en bordel par ce que le texte permet de se faire une meilleure id\u00e9e de l'utilis\u00e9 de chaque partie.","title":"Mardi 9 Mai 2023"},{"location":"jdb.html#mercredi-10-mai-2023","text":"Bon hier je n'ai pas eu le temps de finir la documentation de la recup\u00e8ration d'images et de la calibration. Il faudra donc que je repasse un coup dessus en fin de semaine je pense. Mais la j'aimerais avancer sur la mise en commun du projet, comme la configuration fonctionne plut\u00f4t pas mal je pense que je vais juste vite fait aller commenter les methodes qui ne le sont pas encore et ensuite je vais passer \u00e0 l'impl\u00e9mentation de l'OCR. Je suis presque certain que l'OCR va avoir besoin de plus de r\u00e8glages mais bon on verra bien. Je me rend compte en commentant que la methode de load serait plus efficace avec un tout petit peu plus d'infos de la part du JSON. J'aurais pu ajouter l'offset entre chaque Driver Zone pour eviter un l\u00e8ger drift lors de la reconstruction. Mais bon rien de grave donc je pense que je vais le laisser comme ca pour le moment \u00e0 moins que ca me pose soucis plus tard. J'ai eu quelques soucis avec les images en 4K. Du coup j'ai descendu les variables d'environnement \u00e0 1920x1080 En fait il y a parfois un soucis un peu p\u00e9nible avec l'OCR. Parfois pour un temps comme ci dessous: \"1:45.140\" Le programme ne va pas bien comprendre les ponctuations et il va donner : 1115140 La il y a deux probl\u00e8mes... Le 1:xx.xxx est compris comme 11xxxxx et le 4 s'est transform\u00e9 en 1... J'ai cr\u00e9\u00e9 ce \"petit\" bout de code pour g\u00e8rer les fois ou les '.' et les ':' ont mal \u00e9t\u00e9 interpr\u00eat\u00e9s if ( rawNumbers . Count == 1 ) { //If this code is used it means that its bad ... //The methods that comes are really not that great and are juste quick fixes try { result = Convert . ToInt32 ( rawNumbers [ 0 ]); switch ( windowType ) { case OcrImage . WindowType . Sector : //The usual sector is in this form : 33.456 if ( rawNumbers [ 0 ]. Length == 6 ) { //The '.' has been understood like a number result = 0 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ] + rawNumbers [ 0 ][ 1 ]) * 1000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 3 ] + rawNumbers [ 0 ][ 4 ] + rawNumbers [ 0 ][ 5 ]); } if ( rawNumbers [ 0 ]. Length == 5 ) { //The '.' has been overlooked result = 0 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ] + rawNumbers [ 0 ][ 1 ]) * 1000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ] + rawNumbers [ 0 ][ 3 ] + rawNumbers [ 0 ][ 4 ]); } break ; case OcrImage . WindowType . LapTime : //The usual Lap time is in this form : 1:45:345 if ( rawNumbers [ 0 ]. Length == 6 ) { //The '.' and ':' have been overlooked //I Know Im skipping the cases where there are more than 9 minuts but it happens so rarely that... we dont care result = 0 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]) * 60000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 1 ] + rawNumbers [ 0 ][ 2 ]) * 1000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 3 ] + rawNumbers [ 0 ][ 4 ] + rawNumbers [ 0 ][ 5 ]); } if ( rawNumbers [ 0 ]. Length == 7 ) { //There is two possibilities //Either 1:45.140 has been interpreted as 1145.10 or 1:451140. We will assume its the first one result = 0 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]) * 60000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ] + rawNumbers [ 0 ][ 3 ]) * 1000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 4 ] + rawNumbers [ 0 ][ 5 ] + rawNumbers [ 0 ][ 6 ]); } break ; case OcrImage . WindowType . Gap : //The usual Gap is in this form : + 34.567 if ( rawNumbers [ 0 ]. Length == 5 ) { //The '.' has been overlooked result += Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ] + rawNumbers [ 0 ][ 1 ]) * 1000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ] + rawNumbers [ 0 ][ 3 ] + rawNumbers [ 0 ][ 4 ]); } break ; } if ( rawNumbers [ 0 ]. Length > 6 ) { //The number definitely has been interpreted wrong } } catch { //It can be because the input is empty or because its the LEADER bracket result = 0 ; } } else { //Auuuugh result = 0 ; } ConfigFile = \"./Presets/Clean_2023.json\" ; string gpUrl = \"https://f1tv.formula1.com/detail/1000006688/2023-azerbaijan-grand-prix?action=play\" ; Bon je n'arrive pas \u00e0 faire fonctionner l'OCR sans tout faire crash \u00e0 chaque fois. Je vais abandonner le travail de la journ\u00e9e pour revenir au point initial... C'est tr\u00e8s frustrant mais bon je ne vois pas comment faire mieux. Rien ne marche alors qu'avant ca marchant super sur le projet OCR normal. Va savoir pourquoi m\u00eame comme ca, impossible de faire marcher l'OCR. Il y a un soucis au niveau de l'ASYNC qui me fait crash tout le temps en me disant qu'un objet est deja en train d'\u00eatre utilis\u00e9. Ca marchait nikel dans mes premi\u00e8res version je ne vois pas pourquoi ca p\u00eate maintenant. Je pense que je vois \u00e0 peu pr\u00e8s le soucis. public virtual async Task < DriverData > Decode ( List < string > driverList ) { int sectorCount = 0 ; DriverData result = new DriverData (); Parallel . ForEach ( Windows , async w => { // A switch would be prettier but I dont think its supported in this C# version if ( w is DriverNameWindow ) result . Name = ( string ) await ( w as DriverNameWindow ). DecodePng ( driverList ); if ( w is DriverDrsWindow ) result . DRS = ( bool ) await ( w as DriverDrsWindow ). DecodePng (); if ( w is DriverGapToLeaderWindow ) result . GapToLeader = ( int ) await ( w as DriverGapToLeaderWindow ). DecodePng (); if ( w is DriverLapTimeWindow ) result . LapTime = ( int ) await ( w as DriverLapTimeWindow ). DecodePng (); if ( w is DriverPositionWindow ) result . Position = ( int ) await ( w as DriverPositionWindow ). DecodePng (); if ( w is DriverSectorWindow ) { sectorCount ++; if ( sectorCount == 1 ) result . Sector1 = ( int ) await ( w as DriverSectorWindow ). DecodePng (); if ( sectorCount == 2 ) result . Sector2 = ( int ) await ( w as DriverSectorWindow ). DecodePng (); if ( sectorCount == 3 ) result . Sector3 = ( int ) await ( w as DriverSectorWindow ). DecodePng (); } if ( w is DriverTyresWindow ) result . CurrentTyre = ( Tyre ) await ( w as DriverTyresWindow ). DecodePng (); }); return result ; } Ca c'est ma methode de decoding de chaque Driver Zone. Le message d'erreur me parle d'une windowImage quand il dit qu'un objet est d\u00e9ja utilis\u00e9. Ma conjecture c'est que en essayant de faire toutes les windows en m\u00eame temps. Elles veulent parfois acc\u00e8der \u00e0 l'image principale en m\u00eame temps. Ce qui evidemment pose probl\u00e8me. Je pense que le fix le plus simple serait de faire le traitement sans le parallele quitte \u00e0 exporter ce fonctionnement sur chaque zone en elle m\u00eame pour ne pas perdre trop de performances. Ok je crois que je vois ou est le soucis. En fait dans cette version du programme c'est toujours la premi\u00e8re image qui \u00e9tait juste tout le temps prise et dans la premi\u00e8re image on a une partie des chiffres qui est bloqu\u00e9e par l'UI de la fen\u00eatre... lol... EN FAIT J'avais un soucis dans ma gestion des chiffres mal faits. Visiblement parfois quand je ne prenais pas en compte un :, un LapTime etait compris comme un Gap to leader ou un Secteur Bon j'en ai tellement marre... Je n'arrive tout simplement PAS \u00e0 faire fonctionner l'OCR ca crash tout le temps j'en peux plus. J'ai tent\u00e9 de r\u00e8gler les probl\u00e8mes de mauvaises detections de secteurs et temps au tour qui font crasher l'app : if ( rawNumbers . Count == 2 ) { //ss:ms result = ( Convert . ToInt32 ( rawNumbers [ 0 ]) * 1000 ) + Convert . ToInt32 ( rawNumbers [ 1 ]); if ( result > ( 60000 + 999 )) { if ( windowType == OcrImage . WindowType . LapTime ) { result = 0 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]) * 60000 ; result += Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString ()) * 1000 ; result += Convert . ToInt32 ( rawNumbers [ 1 ]); } if ( windowType == OcrImage . WindowType . Sector ) { int seconds = 0 ; if ( rawNumbers [ 0 ]. Length == 3 ) { //We have one char that we need to delete //For no apparent reason im going to delete the first seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 1 ]. ToString () + rawNumbers [ 0 ][ 2 ]. ToString ()); } else { seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString () + rawNumbers [ 0 ][ 1 ]. ToString ()); } int ms = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString () + rawNumbers [ 0 ][ 1 ]. ToString () + rawNumbers [ 0 ][ 2 ]. ToString ()); result = seconds * 1000 + ms ; } } } Mais toujours impossible de faire fonctionner cette M**** C'est juste infernal. Je pense que je vais encore tout retirer et remplacer par ce que j'ai dans mon projet OCR original. Donc c'est une journ\u00e9e de perdue compl\u00eatement... C'est extr\u00eamement frustrant. Apr\u00e8s des heures de debug j'ai enfin r\u00e9ussi \u00e0 faire fonctionner le programme de temps en temps. Mais j'ai toujours le soucis que l'image ne veut pas changer alors que je fais tout pour et que l'OCR est nulle \u00e0 chier du coup...","title":"Mercredi 10 Mai 2023"},{"location":"jdb.html#jeudi-11-mai-2023","text":"Bon apr\u00e8s une bonne nuit de sommeil je vais reprendre les choses depuis le d\u00e9but. J'ai deux soucis : L'OCR pue du derche L'Image que l'on d\u00e9code ne change pas Pour la premi\u00e8re partie j'ai ma petite th\u00e9orie. Je pense que comme je donne des images 4K alors que le feed est en 1080P, il y a d\u00e9ja un genre d'interpolation qui est faite. Je pense donc qu'il faut que j'adapte mon engine pour qu'il fonctionne avec cette r\u00e9solution. Je me suis demand\u00e9 si ca n'\u00e9tait pas mieux de prendre en compte les deux r\u00e9solutions pour les pc un peu moins bal\u00e8zes et j'ai d\u00e9cid\u00e9 de n'en avoir rien a faire. On verra dans le futur si c'est une feature que je voudrais ajouter mais c'est en dehors du scope du dipl\u00f4me je pense. Pour la seconde partie, je pense qu'il faut que j'aille voir du c\u00f4te de OCR_Decode et de OCR Tester pour voir comment je faisais. Je dois forc\u00e9ment oublier un truc. Bon ca commence mal, quand je vais voir dans le projet OCR_Decode, le changement d'image est exactement le m\u00eame et il fonctionne alors que de mon c\u00f4t\u00e9 ca n'est pas le cas. Alors deux choses. Je me rend compte que le changement d'images n'a AUCUN effet sur la detection de texte, et seconde chose, le d\u00e9calage est trop grand entre les windows. Des que le soucis d'image est r\u00e8gl\u00e9 il va falloir que je change drastiquement ma facon de stocker la config en JSON. Il faut que je conserve les \u00e9carts. Sinon regardez ce que ca donne quand on arrive au dernier pilote : \"Zone de pilote d\u00e9cal\u00e9e\" Je commence \u00e0 devenir FOU. Je n'arrive pas \u00e0 changer cette foutue image wtf... J'ai beau tenter par tous les moyens de la changer par une image noire, l'image semble toujours rester celle du d\u00e9part. Bon j'ai enfin trouv\u00e9 pourquoi et je n'ai pas envie de dire comment j'ai trouv\u00e9... Je pense que l'on a tous droit \u00e0 son petit jardin secret. Maintenant ca veut dire que je peux me focus sur le concept important qui est le changement de la cr\u00e9ation et de la lecture des JSON. Voici un exemple de preset JSON : { \"Main\" : { \"x\" : 40 , \"y\" : 355 , \"width\" : 3784 , \"height\" : 1438 , \"Zones\" : [ { \"DriverZone\" : { \"x\" : 0 , \"y\" : -10 , \"width\" : 3784 , \"height\" : 71 , \"Windows\" : [ { \"Position\" : { \"x\" : 47 , \"y\" : 11 , \"width\" : 72 }, \"GapToLeader\" : { \"x\" : 445 , \"y\" : 13 , \"width\" : 201 }, \"LapTime\" : { \"x\" : 859 , \"y\" : 14 , \"width\" : 221 }, \"DRS\" : { \"x\" : 1094 , \"y\" : 13 , \"width\" : 173 }, \"Tyres\" : { \"x\" : 1270 , \"y\" : 11 , \"width\" : 1452 }, \"Name\" : { \"x\" : 2727 , \"y\" : 11 , \"width\" : 351 }, \"Sector1\" : { \"x\" : 3083 , \"y\" : 10 , \"width\" : 253 }, \"Sector2\" : { \"x\" : 3339 , \"y\" : 14 , \"width\" : 195 }, \"Sector3\" : { \"x\" : 3518 , \"y\" : 14 , \"width\" : 250 } } ] } } ] }, \"Drivers\" : [ \"Perez\" , \"Leclerc\" , \"Sainz\" , \"Alonso\" , \"Stroll\" , \"Russel\" , \"Verstappen\" , \"Zhou\" , \"Ocon\" , \"Hulkenberg\" , \"Hamilton\" , \"Norris\" , \"Tsunoda\" , \"Magnussen\" , \"Piastri\" , \"Albon\" , \"Gasly\" , \"Sargeant\" , \"Bottas\" , \"De Vries\" ] } Je pense que ce qui serait bien ce serait de rajouter un \"offsets\" qui contienne les 19 \u00e9carts restants. Bon... la structure de ma fabrication de JSON etait trop confuse je trouve alors je l'ai compl\u00eatement refaite. J'ai aussi abandonn\u00e9 l'id\u00e9e de faire un fichier le plus petit possible car au final on s'en fiche et le plus important c'est que toutes les windows et les zones soient aux bons endroits. Ca nous fait un fichier d'environs 1300 lignes mais au moins le code pour la serialisation est plut\u00f4t clean : public void SaveToJson ( List < string > drivers , string configName ) { string JSON = \"\" ; JsonObject jsonFileObject = new JsonObject (); //Creating the mainZone object JsonObject mainZoneObject = new JsonObject (); mainZoneObject . Add ( \"x\" , MainZone . Bounds . X ); mainZoneObject . Add ( \"y\" , MainZone . Bounds . Y ); mainZoneObject . Add ( \"width\" , MainZone . Bounds . Width ); mainZoneObject . Add ( \"height\" , MainZone . Bounds . Height ); JsonArray driverZonesArray = new JsonArray (); int DriverID = 0 ; foreach ( Zone driverZone in MainZone . Zones ) { DriverID ++; JsonObject driverZoneObject = new JsonObject (); driverZoneObject . Add ( \"name\" , \"Driver\" + DriverID ); driverZoneObject . Add ( \"x\" , driverZone . Bounds . X ); driverZoneObject . Add ( \"y\" , driverZone . Bounds . Y ); driverZoneObject . Add ( \"width\" , driverZone . Bounds . Width ); driverZoneObject . Add ( \"height\" , driverZone . Bounds . Height ); JsonArray windowsArray = new JsonArray (); JsonObject windowObject = new JsonObject (); foreach ( Window window in driverZone . Windows ) { windowObject . Add ( window . Name , new JsonObject { { \"x\" , window . Bounds . X }, { \"y\" , window . Bounds . Y }, { \"width\" , window . Bounds . Width }, { \"height\" , window . Bounds . Height } }); } windowsArray . Add ( windowObject ); driverZoneObject . Add ( \"Windows\" , windowsArray ); driverZonesArray . Add ( driverZoneObject ); } mainZoneObject . Add ( \"DriverZones\" , driverZonesArray ); JsonArray driversArray = new JsonArray (); foreach ( string driver in drivers ) { driversArray . Add ( driver ); } mainZoneObject . Add ( \"Drivers\" , driversArray ); jsonFileObject . Add ( \"Main\" , mainZoneObject ); JSON = jsonFileObject . ToString (); //Saving the file string path = CONFIGS_FOLDER_NAME + configName ; if ( File . Exists ( path + \".json\" )) { //We need to create a new name int count = 2 ; while ( File . Exists ( path + \"_\" + count + \".json\" )) { count ++; } path += \"_\" + count + \".json\" ; } else { path += \".json\" ; } File . WriteAllText ( path , JSON ); } Et normalement la lecture devrait \u00eatre encore plus simple. En fait c'\u00e9tait pas beaucoup plus simple mais au moins maintenant ca marche. Je vais pas mettre le code de lecture ici car c'est un peu trop long donc il va falloir me croire sur parole. (Ou aller sur Git) Bon bah on est au m\u00eame endroit qu'hier... Bon pour demain le plan de bataille ca va \u00eatre : Changer compl\u00eatement la methode \"GetTimeFromPng\" pour qu'elle prenne en compte toutes les possibilit\u00e9s de bugs et d'oubli de '.' ou de ':' mais pas selon le nombre de blocs mais selon le type de temps que l'on cherche Pour le moment je regarde le nombre de blocs et si il y en a deux alors c'est que c'est un temps de secteur. En fait non cela peut aussi \u00eatre un temps au tour qui a rat\u00e9 un point. Il faut que je bosse juste un peu vite fait la dessus et que j'arr\u00eate de putain de crasher d\u00e8s que un truc est pas au bon format. Ensuite quand ca aura arr\u00eat\u00e9 de crasher je vais reprendre l'OCR et voir pourquoi les resultats sont nuls a chier comme ca. Et le but c'est que demain soir j'ai une reconnaissance de caract\u00e8res plus proche de ce que j'avais dans d'autres projets... J'y croit 0 mais bon l'espoir fait vivre comme on dit.","title":"Jeudi 11 Mai 2023"},{"location":"jdb.html#vendredi-12-mai-2023","text":"Bon aujourd'hui il faut que ca marche. On va y aller par \u00e9tape. Je vais revoir toutes les methodes d'OCR et essayer de r\u00e9apliquer les filtres differemment et revenir au point de d\u00e9part. Avant de commencer je note plusieurs soucis avec les premiers tests : Les positions des pilotes ont l'air pas mal (Pas besoin de tout changer mais peut-\u00eatre simplement checker que les filtres sont bons) L'\u00e9cart avec le leader est \u00e9tonnamment pas mal aussi Le temps au tour est tout simplement horrible. Aucun n'est juste et de tr\u00e8s loin m\u00eame si les d\u00e9cimales ne sont pas forc\u00e9ment loin Le DRS je n'ai pas eu l'occasion de bien le tester mais je dirais que ca devrait \u00eatre bon (\u00e0 verifier quand le reste sera bon) Les pneus ne sont \u00e9tonnamment pas SI horribles, m\u00eame si parfois les lettres sont prises comme des chiffres Les noms de pilotes sont tr\u00e8s bon (pas \u00e9tonnant vu le syst\u00e8me de distance de Levenstein donc ca m\u00e9rite quand m\u00eame un petit check) Les secteurs sont en g\u00e9n\u00e9ral horribles mais pas toujours. C'est peut-\u00eatre un soucis de d\u00e9cimale ou des 4 qui se transforment en 1 J'ai remarqu\u00e9 que les 4 sont souvent pris comme des 1. Peut-\u00eatre que en ayant des images 4K l'interpolation est un peu diff\u00e9rente de ce que j'ai l'habitude de voir. Mais donc le plan aujourd'hui c'est de checker tous ces points et les faire fonctionner (youpi...) J'ai d\u00e9sactiv\u00e9 toutes les methodes de cette facon : int sectorCount = 0 ; DriverData result = new DriverData (); foreach ( Window w in Windows ) { // 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); result . Name = \"Unknown\" ; if ( w is DriverDrsWindow ) //result.DRS = (bool)await (w as DriverDrsWindow).DecodePng(); result . DRS = false ; if ( w is DriverGapToLeaderWindow ) //result.GapToLeader = (int)await (w as DriverGapToLeaderWindow).DecodePng(); result . GapToLeader = 0 ; if ( w is DriverLapTimeWindow ) //result.LapTime = (int)await (w as DriverLapTimeWindow).DecodePng(); result . LapTime = 0 ; if ( w is DriverPositionWindow ) //result.Position = (int)await (w as DriverPositionWindow).DecodePng(); result . Position = 0 ; if ( w is DriverSectorWindow ) { sectorCount ++; if ( sectorCount == 1 ) //result.Sector1 = (int)await (w as DriverSectorWindow).DecodePng(); result . Sector1 = 0 ; if ( sectorCount == 2 ) //result.Sector2 = (int)await (w as DriverSectorWindow).DecodePng(); result . Sector2 = 0 ; if ( sectorCount == 3 ) //result.Sector3 = (int)await (w as DriverSectorWindow).DecodePng(); result . Sector3 = 0 ; } if ( w is DriverTyresWindow ) //result.CurrentTyre = (Tyre)await (w as DriverTyresWindow).DecodePng(); result . CurrentTyre = new Tyre ( Tyre . Type . Undefined , 0 ); } return result ; Le but c'est que ensuite je puisse y aller \u00e9tape par \u00e9tape. Position : Alors pour cette reconnaissance je dirais que la 4K fait des merveilles qui permettent de retirer du processing. La position apr\u00e8s un simple Treshold est assez bien reconnue et la dilataion et/ou Erosion ne sont pas nescessaires finalement. On va donc pouvoir gagner un certain temps et c'est un bon signe pour la suite. On peut aussi noter que quand un pilote est hors course toutes ses valeurs sont gris\u00e9es et sa position est prise comme un -1 Sectors, alors j'ai refait toute la partie qui concerne les secteurs et qui les nettoie. Et je me suis rendu compte qu'ils \u00e9taient bien souvent juste, le seul truc c'est que ils s'affichent de mani\u00e8re un peu sp\u00e9ciale. En fait dans la page de la F1TV les secteurs peuvent faire plus de 60 secondes sans passer sur un affichage de minutes. Ce qui fait que c'est un peu bizarre \u00e0 regarder mais c'est parfaitement juste. J'ai aussi pu simplifier la reconnaissance gr\u00e2ce \u00e0 l'image de meilleure qualit\u00e9 et maintenant les temps de secteur sont plut\u00f4t corrects. En fait le plus long et complexe c'est de pr\u00e9voir les cas particuliers ou un '.' a \u00e9t\u00e9 mal interpr\u00eat\u00e9... Et en parlant de ca, je vais aller m'occuper des temps au tour qui ajoutent une couche de complexit\u00e9 avec un '.' ET un ':' qui peuvent \u00eatre oubli\u00e9s... Oh et j'y pense, un truc malin pourrait \u00eatre de comparer les temps au tour et les temps de secteur. Ils devraient concorder normalement je pense. Ok je viens de finir la gestion des temps au tour... J'ai un code de genre 170 lignes pour juste nettoyer le resultat dans le cas ou des '.' n'ont pas \u00e9t\u00e9 trouv\u00e9s ou des ':' ont \u00e9t\u00e9 n'ont pas \u00e9t\u00e9 trouv\u00e9 ou si l'un des deux s'est transform\u00e9 en chiffre etc.. etc.. etc.. Le soucis c'est que la maintenant je me rend compte que les \u00e9carts entre les pilotes vont juste \u00eatre un enfer \u00e0 nettoyer... Ils peuvent aussi bien \u00eatre \"0.760\" comme \"1:34.456\" du coup... je sais pas vraiment comment faire pour tout nettoyer. Je pense que je vais juste en avoir rien \u00e0 faire et tant pis si de temps en temps c'est pas g\u00e9nial. Bon du coup j'ai pas pris en compte TOUS les cas possibles mais d\u00e9ja un certain nombre et c'est d\u00e9ja pas mal. Sur les diff\u00e9rents Grand Prix d'exemples ca a l'air de plut\u00f4t bien tourner ! Mais ca demande tellement de tests et de code que c'est un peu ridicule... La methode \"GetTimeFromPng\" fait d\u00e9ja presque 430 lignes \u00e0 cause de tous les cas possibles et tous les try catch. Ca peut para\u00eetre peu \u00e9l\u00e9gant mais j'ai essay\u00e9 de mettre des commentaires un peu partout pour permettre \u00e0 nimporte qui de comprendre ce qui se passe. J'ai aussi pu faire les pneus et maintenant (roulement de tambour) Ca marche (presque) En fait j'ai des soucis parfois quand les pneus sont un peu cach\u00e9s dans les permiers tours : Pneus cach\u00e9s Le soucis c'est que du coup le chiffre est un peu illisible... mais je pense que avec l'historique il devrait y avoir moyen de ne pas prendre en compte les chiffres de pneus pendant cinq tours apr\u00e8s le changement de pneu ou de simplement tenter de faire les calculs de pneus. Pour conclure la journ\u00e9e je pense que je devrais avoir le temps de faire un syst\u00e8me qui permet de refresh \u00e0 volont\u00e9. Par contre je viens de d\u00e9couvrir que quand un temps de secteur est en couleur on arrive pas \u00e0 le lire. Ah et la detection prend un peu moins de trois secondes sur mon pc je crois. Mais c'est seulement si les driver zones sont faites en m\u00eame temps mais en faisant ca de temps en temps ca crash et \u00e0 chaques fois c'est d'un endroit diff\u00e9rent du coup je comprend pas vraiment. Sinon ca prend dix secondes.","title":"vendredi 12 Mai 2023"},{"location":"jdb.html#lundi-15-mai-2023","text":"Aujourd'hui c'est journ\u00e9e poster et visites. Comme on va avoir des visites de premi\u00e8res ann\u00e9es voire de terminales et que le soir c'est visite des parents. Je pense que j'ai finit de tout regrouper (\u00e0 part \u00e9videmment le traitement et le stockage des donn\u00e9es) ce qui veut dire que je suis pas dans une superbe posture. Il va falloir que je sois tr\u00e8s efficace dans la partie stockage de donn\u00e9es et mise en place du mod\u00e8le si je veux avoir une chance de rendre un joli travail de dipl\u00f4me. (et m\u00eame comme ca je peux voir que le temps commence \u00e0 manquer) Dans l'id\u00e9al je devrais avoir termin\u00e9 la partie stockage jeudi... Ce qui veut dire que je n'ai que trois jours pour le faire et que en plus jeudi je dois travailler depuis la maison. Ca va pas \u00eatre simple. Mais aujourd'hui je vais m'occuper d'adapter la documentation de l'OCR et faire la documentation de toute la partie r\u00e9cup\u00e8ration d'images et de la calibration. Bon au final la journ\u00e9e a \u00e9t\u00e9 un peu difficile. On a pas vraiment pu travailler l'apr\u00e8s midi car il a fallu pr\u00e9senter le projet environs 10 fois \u00e0 toutes les classes et \u00e0 des parents voire futurs experts. Les d\u00e9mos ont plut\u00f4t bien fonctionn\u00e9es j'en suis assez content. Mais ca veut dire que la partie doc a pas forc\u00e9ment pu \u00eatre totalement compl\u00eat\u00e9e mais demain il va falloir que je m'occupe de la suite du projet.","title":"Lundi 15 Mai 2023"},{"location":"jdb.html#mardi-16-mai-2023","text":"Bon aujourd'hui c'est la partie stockage qui doit \u00eatre faite. Il y a plusieurs solutions possibles \u00e0 ce probl\u00e8me. Mais comme je n'ai besoin que d'une base de donn\u00e9e locale et que je ne veux pas que chaque utilisateur doive installer un serveur sur sa machine je pense que je vais utiliser une base de donn\u00e9es SQLITE. Il y a eu une petit intervention de mr Bonvin qui est venu me donner une id\u00e9e pour la partie OCR. En fait j'avais un soucis quand je voulais d\u00e9coder du texte de couleur. M\u00eame en appliquant un filtre de gris je n'arrivais pas \u00e0 faire reconnaitre les chiffres. Et il m'a dit que une bonne id\u00e9e cela pourrait de prendre la valeur max de chaque channel et de la faire appliquer \u00e0 tous ce qui blanchit assez bien l'image. J'ai d\u00e9cid\u00e9 d'exag\u00e8rer le blanchiment et cela donne des r\u00e9sultats plut\u00f4t... int\u00e9ressants... \"Filtre vanish oxy action sur un secteur violet\" Le soucis c'est que le violet est une couleur quand m\u00eame assez sombre alors il va falloir que je fasse un syst\u00e8me de treshold un peu sp\u00e9cial qui soit un peu plus sympa et qui prenne plus facilement des couleurs plus basses. Une methode \u00e0 laquelle j'ai pens\u00e9 pour detecter dans quel tour chaque pilote est serait de garder en m\u00e9moire toutes les infos de chaques pilotes au fur et \u00e0 mesure, et d\u00e8s qu'on ne recoit plus d'infos des secteurs ou que le temps au tour a chang\u00e9 on peut savoir qu'il faut passer au tour suivant. Pour detecter les arr\u00eats aux stands je peux essayer de detecter un changement de type de pneus ou de nombre de tours detect\u00e9s sur le m\u00eame pneu Au d\u00e9part je me disais que je pourrais peut-\u00eatre faire une base de donn\u00e9e SQLITE locale qui puisse \u00eatre reprise d'un Grand Prix \u00e0 un autre. Mais je me suis dit que de faire des statistiques inter Grand Prix \u00e9tait un peu en dehors du scope du projet. La base de donn\u00e9e sera donc cr\u00e9\u00e9e \u00e0 chaque d\u00e9marrage de l'app La mani\u00e8re dont je vois les choses en ce moment est qu'on aie deux sources de donn\u00e9es dans l'affichage final. On aurait une partie des infos qui seraient en direct depuis la detection : Les ecarts entre pilotes La position des pilotes Le dernier temps au tour Les derniers secteurs Les pneus Mais on aurait aussi des rubriques cr\u00e9\u00e9es de toutes pi\u00e8ces par des infos qui viennent de la BD Voici les rubriques qui pourraient \u00eatre int\u00e9ressantes \u00e0 voir dans l'interface finale : Les 3 ou 5 pilotes les plus rapides ces cinq derniers tours Le pilote qui a le plus fait de d\u00e9passements Les batailles en cours Les 3 pilotes les plus lents Un classement pond\u00e9r\u00e9 avec les 20s de moins pour tous les pilotes qui ne se sont pas encore arr\u00eat\u00e9s En gros l'id\u00e9e serait que on update une fois par tour et par pilote la base de donn\u00e9e avec des infos comme le temps au tour, le type de pneu etc... Voici les trois tables que je vais cr\u00e9er : Drivers Colonne Type de Data Description Tag ID INTEGER ID du pilote PRIMARY Name VARCHAR Nom du pilote NOT NULL Pitstops Colonne Type de Data Description Tag Lap INTEGER Tour durant lequel le Pitstop a \u00e9t\u00e9 effectu\u00e9 PRIMARY DriverID INTEGER Pilote qui a effectu\u00e9 le Pitstop PRIMARY Tyre VARCHAR Pneu chauss\u00e9 par le pilote NOT NULL Stats Colonne Type de Data Description Tag Lap INTEGER Tour durant lequel le Pitstop a \u00e9t\u00e9 effectu\u00e9 PRIMARY DriverID INTEGER Pilote qui concern\u00e9 PRIMARY Tyre VARCHAR Pneu chauss\u00e9 par le pilote NOT NULL LapTime INTEGER Temps au tour (MS) NOT NULL Sector1 INTEGER Temps du secteur 1 (MS) NOT NULL Sector2 INTEGER Temps du secteur 2 (MS) NOT NULL Sector3 INTEGER Temps du secteur 3 (MS) NOT NULL GapToLeader INTEGER Ecart avec le leader (MS) NOT NULL Position INTEGER Position pilote NOT NULL Ca n'est pas forc\u00e9ment d\u00e9finitif mais je pense que c'est d\u00e9ja un bon d\u00e9but pour faire des rubriques sympa. Je suis en train de tenter d'impl\u00e9menter le code pour permettre ensuite d'ajouter et retirer des choses facilement.","title":"Mardi 16 Mai 2023"},{"location":"jdb.html#mercredi-17-mai-2023","text":"Aujourd'hui le but c'est de remplir la base SQLITE avec des infos. Si j'arrive \u00e0 tout remplir alors ca devrait pas \u00eatre trop compliqu\u00e9 de venir faire des requ\u00e8tes qui donnent de bonnes infos. Mais la probl\u00e9matique principale va \u00eatre de d\u00e9cider QUAND ins\u00e8rer des choses dans la base de donn\u00e9e. Je pense que le meilleur moyen serait de garder une liste de DriverData par pilote en piste qui puisse contenir toutes les data que l'on recoit. Et \u00e0 chaques fois que l'on veut ajouter \u00e0 cette liste on v\u00e9rifie si un tour a \u00e9t\u00e9 fait pour envoyer les data pr\u00e9c\u00e9dente et r\u00e9initialiser la liste. Il faut donc une liste de 20 listes de DriverData et une liste de int qui repr\u00e9senteront le num\u00e9ro du tour dans lequel chaque pilote se trouve. Pour detecter un arr\u00eat je pense que la meilleure mani\u00e8re est de regarder si le pilote a chang\u00e9 de place ou de type de pneu. Si je prend que les fois ou le pilote change place ET de pneus alors certains arr\u00eats pour ceux qui sont loins devant ou loins derri\u00e8re pourraient ne pas \u00eatre detect\u00e9s. Et si je ne prend que le changement de pneus cela pose un soucis car un pneu pourrait avoir \u00e9t\u00e9 chang\u00e9 pour un autre du m\u00eame type. Et parfois les valeurs de tours faits avec le pneu ne sont pas toujours bien lues et parfois sont compliqu\u00e9es \u00e0 retrouver car tous les pneus ne sont pas neuf quand ils sont chauss\u00e9s. Je crois que la detection de tours et des arr\u00eats aux stands est sur la bonne voie. Le seul soucis que j'ai c'est que pour faire du debug je suis un peu oblig\u00e9 d'attendre pendant 10min si je veux avoir de quoi faire des stats un peu sympa. Je me rend compte que parfois j'obtiens des r\u00e9sultats un peu bizarres mais que c'est la f1TV qui les donne. Par exemple de cas ou Alex Albon n'a que deux tours sur ses pneus alors que tout le monde devant et derri\u00e8re lui en a 3 et que on est au tour 4. \"wtf...?\" Ah et aussi parfois quand les pilotes se d\u00e9passent on se retrouve dans des situations plut\u00f4t rigolotes : \"Wtf...??\" COMMENT JE FAIS POUR DETECTER CA WIONDVIDNJDODVNSDIC Bon je me rend compte que clairement si je veux que mes data soient plus utiles il faudrait que je fasse un tout petit peu plus de taff sur quelques points dans l'OCR. Le nombre de tours des pneus (Les num\u00e9ros sont vraiment mal detect\u00e9s et parfois m\u00eame la couleur est pas dingue) Les 4 qui sont pris pour des 1 ou des 11 (pour les temps et les pneus) apr\u00e8s un test de plus longue haleine je suis content de voir que au moins mon programme peut tourner plus d'une heure sans crasher et qu'il peut \u00eatre fiable quand il veut.","title":"Mercredi 17 Mai 2023"},{"location":"jdb.html#jeudi-18-mai-2023","text":"Aujourd'hui c'est t\u00e9l\u00e9Travail forc\u00e9 et j'\u00e9tais scens\u00e9 aller au Grand Prix d'Imola ce qui malheureusement ne pourra pas se faire pour des raisons d'inondations. En effet la r\u00e9gion est clairement pas en \u00e9tat de recevoir un Grand Prix de Formule 1 et donc ce weekend c'est maison. Le but du jour c'est d'avancer la doc et de tenter d'am\u00e9liorer l'OCR pour que Lundi il soit relativement facile d'avancer sur l'interface de l'app finale. Je suis en train d'explorer une methode de detection de bords de sobel. Le seul soucis c'est que les r\u00e9sultats sont bons mais avec un vide au milieu des chiffres. Cela veut dire que parfois le temps est mal detect\u00e9. Mais il semble que pour le reste du temps cela se passe plut\u00f4t bien. Ca vaut peut \u00eatre le coup de modifier la gestion des erreurs. \"Artefacts de detection de sobel\" En fait le soucis avec ces artefacts c'est que parfois le temps au tour n'est tout simplement pas detect\u00e9. Dans l'exemple ci dessus, la reconnaissance de caract\u00e8res ne trouve tout simplement rien. Il faut donc que je trouve un moyen de corriger ces soucis. Il semblerait que en appliquant un tresholding un peu plus s\u00e9v\u00e8re en amont on arrive \u00e0 r\u00e9duire les artefacts \"Artefacts all\u00e8g\u00e9s\" ; Je crois qu'il faut faire attention avec les 'Bitmap.save' quand on utilise de l'asynchrone. le GDI+ aime pas des masses.","title":"Jeudi 18 Mai 2023"},{"location":"jdb.html#lundi-22-mai-2023","text":"BON ! Il ne reste plus beaucoup de temps ! Selon le planning cette semaine est la derni\u00e8re semaine de programmation. Il va donc falloir CHARBONNER !! Il ne me reste plus que une t\u00e2che \u00e0 vraiment faire (\u00e0 part les tests mais euuuu voila bon). J'essaie d'impl\u00e9menter un peu plus d'error handling mais c'est pas facile... Il y a tellement de choses qui peuvent mal tourner c'est infernal. Une chose qui serait bien serait de rajouter des points d'attente variables dans le code de l'emulateur un peu partout pour eviter de se retrouver bloqu\u00e9 \u00e0 chaque fois. L'int\u00e9r\u00eat serait que des gens avec une moins bonne connexion pourraient quand m\u00eame profiter du programme sans qu'il crashe 300 fois. Ce qui est frustrant c'est que va savoir pourquoi, maintenant, on arrive quasi jamais a avoir la page data... Genre sans deconner c'est une fois sur 5 que l'emulateur nous ressort la page Data et pas juste le feed. C'est absolument infernal. Je ne comprends pas pourquoi en plus. L'emulateur arrive bien \u00e0 cliquer sur le bon bouton mais m\u00eame comme ca ca ne veut pas. CA NA AUCUN SENS BORDEL. Sur une image Jjai de supers resultats pour les temps au tour mais d\u00e8s que l'image change PAF plus aucun temps n'est detect\u00e9. Ah non c'est bon c'est juste que VA SAVOIR POURQUOI les images sont en putain de resolution DEGEULASSE. Je ne comprends pas pourquoi ce matin particuli\u00e8rement le projet marche si mal. Par ce que d\u00e8s que l'image revient \u00e0 une r\u00e9solution normale c'est bon. Un autre soucis que j'ai est que je n'arrive pas \u00e0 paralleliser l'OCR ce qui fait que elle peut prendre parfois plus de 15 secondes. Et le probl\u00e8me avec ca c'est que la detection de tours et de pitstop est grandement impact\u00e9e si on a pas assez de data assez souvent. Je vais me focus sur le reste en attendant mais d\u00e8s que M.Bonvin apparait dans les parages je vais devoir l'alpaguer. J'ai ajout\u00e9 la possibilit\u00e9 d'essayer plusieurs fois de trouver le bouton fullscreen et de cliquer dessus plut\u00f4t que d'attendre dix secondes comme un con et esp\u00e8rer que ca fonctionne. Mais si apr\u00e8s 15 secondes d'essais il n'y arrive pas cela fait quand m\u00eame p\u00eater une erreur. Je pense que je vais m'occuper de la page de configuration maintenant. Voici \u00e0 quoi ressemblait la page de settings ce matin quand je suis arriv\u00e9. \"Ancienne page de settings\" Comme je pense que l'UI de cette page ne va pas vraiment changer d'ici la fin du projet je peux me permettre de lui faire une petite beaut\u00e9 car apr\u00e8s je ne pense pas y retoucher. Pour ca j'ai plusieurs \u00e9tapes comme choisir une palette de couleur, retirer l'inutile et choisir judicieusement le placement des items sur la form pour que cela soit le plus intuitif possible. J'y pense, il fuadrait peut-\u00eatre que je me trouve un logo ca pourrait rendre bien. Voila alors j'ai chang\u00e9 un tout petit peu ce \u00e0 quoi ressemble la page de settings et j'ai ajout\u00e9 du responsive pour que le user puisse mettre l'application en plein \u00e9cran. \"Nouvelle page de configuration\" Mais il manque un peu de couleurs et de d\u00e9tails pour que cela rende vraiment bien. Et apr\u00e8s quelques tentatives on se retrouve avec une page plut\u00f4t sympa je trouve : \"Nouvelle page de config\" Et elle est responsive : \"Nouvelle page de config en plein \u00e9cran\" Et j'ai fait quelques changements pour ce qui est des zones qui s'affichent pour qu'on les voie mieux. \"Nouvelle page de config en action\" Je trouve que franchement ca rend pas mal. Le reste de l'app sera dans ce style. J'ai mis pas mal de temps \u00e0 cr\u00e9er cette page, mais je pense que c'est important que la page de config soit propre. Et en plus tout le temps que j'ai pass\u00e9 ici n'est pas perdu car ensuite j'aurai simplement \u00e0 suivre les m\u00eames directives de style pour le reste de l'UI. Il faut aussi savoir que Windows Form n'est ps forc\u00e9ment le meilleur outil pour travailler avec le design. Truc tout b\u00eate par exemple qui m'a fait perdre 30 minutes. Il est impossible de retirer les bordures des objets \"GroupBox\". Ce que j'ai donc du faire ca a \u00e9t\u00e9 de dessiner un rectangle autour de la couleur du background pour que l'on ne voit plus les bordures et ensuite j'ai du redessiner le texte pour qu'il puisse s'afficher quand m\u00eame. C'est pleins de petites choses comme ca qui sont plut\u00f4t p\u00e9nibles et qui font perdre du temps mais je pense que c'est rentable de s'y attarder. Maintenant ce que je vais faire aujourd'hui et demain c'est l'affichage general de l'app. Je pense que je vais commencer par mettre des placeholder de l'app finale comme ca je saurai quoi implementer comme methodes de r\u00e9cup\u00e8ration demain. J'aimerais quand m\u00eame faire une interface sympa m\u00eame si les data sont pas parfaites. Par ce que je me dis que au pire si je montre une interface qui donne des infos inexacte mais qui a la bonne logique c'est toujours mieux que de ne pas montrer ce que ca pourrait faire avec des donn\u00e9es un peu plus int\u00e8gres. Autre point \u00e0 noter, je me suis rendu compte que ca pourrait \u00eatre potentiellement pas mal de trouver un moyen rapide de lancer l'appli avec un Grand Prix. Genre permettre de selectionner le preset et l'URL du Grand Prix sans avoir \u00e0 passer par la page de configuration. Je me suis rendu compte que c'\u00e9tait super chiant de devoir \u00e0 chaque fois le faire (m\u00eame si je me rends compte que normalement un user ne devrait pas lancer l'app autant) Voila ue premi\u00e8re version de l'App avec tous les placeholders : \"Premi\u00e8re version de la page principale\" Et c'est tout pour aujourd'hui ! Ce fut une journ\u00e9e remplie.","title":"Lundi 22 Mai 2023"},{"location":"jdb.html#mardi-23-mai-2023","text":"Aujourd'hui le but c'est de remplir le framework de hier avec les bonnes Data. Je ne sais pas si je peux tout finir en un jour mais on va essayer. Bon j'ai eu une discussion anim\u00e9e avec M.Bonvin et il semble que je sois oblig\u00e9 de refaire \u00e0 peu pr\u00e8s tout mon code pour le rendre ne serait-ce qu'un peu optimis\u00e9. Bon au final j'ai perdu 6H de travail \u00e0 tenter de convertir mon code dans une version un peu plus optimis\u00e9e... Mais je me rend compte que c'est juste impossible... Il me faudrait au moins plusieurs jours pour faire correctement ce refactor et donc je vais tout simplement faire un git restore... C'est extr\u00eament frustrant mais bon... Pas le choix il semble. J'ai du \u00e9crire au moins 600 lignes de code et tout pars en fum\u00e9e. C'est une d\u00e9bauche d'\u00e9nergie absolument ph\u00e9nomenale. Apr\u00e8s ca valait le coup de tenter je pense. (J'ai envie de mourir) EN FAIT C'EST BON !! Il fallait juste que je croie en mon code original !!! J'ai r\u00e9ussi \u00e0 paralelliser mon ancien code. Il ne manquait presque rien mais M.Bonvin voulait absolument que je change le reste. Maintenant j'ai une detection qui se fait en quelques secondes c'est genial.","title":"Mardi 23 Mai 2023"},{"location":"jdb.html#mercredi-24-mai-2023","text":"Alors hier je n'ai pas bien eu le temps d'expliquer ce que voulait que M.Bonvin. En fait mon programme actuellement utilise un d\u00e9coupage qui peut par\u00e2itre complexe. Et de par sa nature, il pensait qu'il \u00e9tait simplement impossible de paralelliser le traitement car trop complexe et trop couteux. Il voulait donc que je passe sur un traitement plus simple. L'id\u00e9e \u00e9tait que on s'occupe dabord de faire une liste de toutes les Windows et de les traiter toutes \u00e0 la fois pour \u00e9viter que les boucles soient trop complexes. Sauf que pour impl\u00e9menter un truc pareil c'est \u00e9norm\u00e9ment de code car cela va \u00e0 l'encontre totale de la facon dont mon projet fonctionne actuellement. Mais comme j'\u00e9tais ouvert \u00e0 d'autres solutions. J'ai pass\u00e9 six heures \u00e0 tenter de l'impl\u00e9menter. Il en aurait fallu au minimum deux jours soyons clair. Et en fait on s'est retrouv\u00e9s devant pleins de probl\u00e8mes qui ne se posent pas dans mon architecture originale. Par exemple. On a pas trouv\u00e9 de methode simple pour d\u00e9couper les images des fen\u00eatres de mani\u00e8re thread safe. Il aurait donc fallu ajouter des boucles en pr\u00e9alable pour tout d\u00e9couper et le faire de mani\u00e8re s\u00e9quencielle. Ensuite vient le probl\u00e8me que si on traite toutes les donn\u00e9es dans des boucless paralelle on perds leur position originale donc il faut faire une classe pour stocker les r\u00e9sultats temporaires. Il y a aussi le soucis que les Windows ont certe une position mais elle est relative au parent et \u00e0 l'image parente. Donc il faudra faire un syst\u00e8me qui convertis les windows en position absolue sur l'image. Ca peut para\u00eetre \u00eatre de simples changements mais deja il y en a pas mal d'autres et franchement m\u00eame si l'id\u00e9e originale aurait pu simplifier les choses. Les sacrifices que l'on doit faire pour la faire marcher sont juste trop moches et \u00e0 mon avis ne sont pas du tout aussi logiques que mon d\u00e9coupage original. Cette exp\u00e9rience m'a quand m\u00eame permis de me rendre compte des endroits dans mon code qui sont plus ou moins difficile \u00e0 maintenir et cela m'a fait me rendre compte que ma solution n'\u00e9tait pas forc\u00e9ment la plus simple pour tout le monde mais que mine de rien elle peut \u00eatre efficace. La je suis en train de rajouter les routes pour la vue. Je me suis dit que ca serait une bonne id\u00e9e de permettre aux users de cliquer sur un pilote pour avoir ses infos. Mais je me suis dit que ce qui serait encore plus cool serait de pouvoir cliquer sur un des temps au tour d'un pilote et qu'une petite fen\u00eatre s'ouvre pour indiquer les temps par secteurs. Mais en faisant ca je me rends compte qu'il y a quelques soucis dans la facon que je conserve les infos dans la DB et je peux voir directement quand la reconnaissance a du mal avec certains pilotes ou des positions. Ca arrive plus souvent que ce que je voudrais que un pilote soit mal detect\u00e9. Mais ce qui est dr\u00f4le c'est que c'est parfois sur une deux voire trois reconnaissance que le pilote n'est plus reconnu mais ensuite tout va bien. Il faut que je travaille un peu plus sur le filtrage de ces donn\u00e9es limites et peut-\u00eatre de voir si la reconnaissance de la position pourrait \u00eatre un peu v\u00e9rifi\u00e9e. Bon pour \u00eatre honn\u00eate je ne pense pas que le code qui concerne l'affichage soit le meilleur code que j'aie pu produire dans ma vie de d\u00e9veloppeur mais en m\u00eame temps je n'ai pas forc\u00e9ment le temps de le rendre magnifique. La le but est simplement que tout marche. (Et c'est un peu la m\u00eame phylosophie dans tout le reste du projet lol) Demain il me reste pas mal de choses \u00e0 faire et c'est la derni\u00e8re journ\u00e9e ou je peux les faire. Rendre la form plus jolie et changer les couleurs Rendre la form Responsive Ajouter les bons messages d'erreur qui vont bien Modifier les messageBox d'erreur pour qu'elles soient plus agr\u00e9ables \u00e0 utiliser Clean un peu le code mod\u00e8le vue controller Si j'ai le temps ajouter les bons commentaires les bonnes ent\u00eates partout","title":"Mercredi 24 Mai 2023"},{"location":"jdb.html#jeudi-25-mai-2023","text":"Bon bah le but aujourd'hui c'est de finaliser un peu le projet car la semaine prochaine c'est doc. Pas grand chose \u00e0 dire. J'ai pass\u00e9 la journ\u00e9e \u00e0 fix des petits bugs par ci par la. Voici des exemples de ce \u00e0 quoi ressemble l'app \u00e0 la fin de la journ\u00e9e : \"Screenshot de la page principale\" \"Screenshot de la page principale\" On se rend jamais compte mais c'est tellement long de r\u00e8gler chaque petit soucis un par un. Il y a tellement de possibilit\u00e9s de choses qui peuvent mal tourner ou qui ont un comportement diff\u00e9rent selon l'ordre dans lequel on fait les choses. Mais dans l'ensemble, m\u00eame si on est pas sur la meilleure interface que l'on aie vu dans l'histoire. Je trouve que elle fait quand m\u00eame le taff.","title":"Jeudi 25 Mai 2023"},{"location":"jdb.html#vendredi-26-mai-2023","text":"Aujourd'hui c'est d\u00e9part pour Monaco mais comme l'avion etait bien en retard j'ai pu avancer sur le nettoyage du code.","title":"Vendredi 26 Mai 2023"},{"location":"jdb.html#grand-prix-de-monaco","text":"[Insert photos]","title":"Grand Prix de Monaco"},{"location":"jdb.html#lundi-29-mai-2023","text":"Mon vol pour Geneve hier soit a \u00e9t\u00e9 annul\u00e9 et je dois donc prendre une deviation car tous les vols pour Geneve sont pleins. Je dois partir a 7h30 pour prende l'avion de 9H pour Nantes et de la bas je dois prendre un avion \u00e0 17h pour arriver \u00e0 18h30 \u00e0 l'a\u00e9roport de Geneve. C'est pas pratique car j'avais pr\u00e9vu d'avancer aujourd'hui et je suis oblig\u00e9 d'avancer comme je peux dans l'a\u00e9roport. J'avance encore sur le nettoyage rapide du code. Le but est que demain je puisse sortir la premi\u00e8re release en Beta et que je mette vraiment serieusement \u00e0 la Documentation. M.Jayr m'a \u00e9galement demand\u00e9 de lui donner le document d'\u00e9valuation interm\u00e9diaire. Il ne faut pas que j'oublie demain. J'ai mis des notes sur mon code au cas ou des gens viennent \u00e0 le lire. J'y d\u00e9cris les choses que j'aurais fait diff\u00e9remment ou qui pourraient para\u00eetre bizarre au tout venant.","title":"Lundi 29 Mai 2023"},{"location":"jdb.html#mardi-30-mai-2023","text":"Bon aujourd'hui je dois encore avancer sur la partie nettoyage de code et avant de sortir la premi\u00e8re release Beta je vais tenter d'installer le projet sur un autre pc pour voir ce que je n'ai pas mentionn\u00e9 dans mon ReadMe. Bon j'ai pu rendre \u00e0 M.Jayr mon evaluation interm\u00e9diaire et j'ai fait les derni\u00e8res modifications sur le projet pour que je puisse sortir une release correcte. Maintenant je vais me mettre \u00e0 la documentation. Il va falloir que je revoie ce que j'ai d\u00e9ja \u00e9crit en ce qui concerne l'OCR car j'ai fait des modifications depuis et j'ai ajout\u00e9 la methode de SOBEL. Il faut peut-\u00eatre aussi que je parle vite fait dans la partie Emulation que j'ai du changer les variables d'environnement pour faire fonctionner le syst\u00e8me en 4K.","title":"Mardi 30 Mai 2023"},{"location":"jdb.html#mercredi-31-mai-2023","text":"Doc","title":"Mercredi 31 Mai 2023"},{"location":"jdb.html#jeudi-1-juin-2023","text":"Bon je me suis rendu compte que je n'avais fait vraiment aucuns tests et que c'est franchement bof. Je pense que ce que je devrais faire pour faire des tests unitaires c'est prendre des exemples de chaque type de windows possibles en plusieurs exemplaires. Ensuite je note le r\u00e9sultat que j'attends et je regarde si ca me retourne la bonne valeur. Mais ca veut dire que ca va me prendre pas mal de temps de tout mettre en place mais ca m'aurait s\u00fbrement fait gagner beaucoup de temps si je l'avais fait d\u00e8s le d\u00e9but... Je pense que une bonne id\u00e9e serait de prendre trois Grand Prix et de prendre une photo de chaque type de window au d\u00e9but et \u00e0 la fin. Plus je regarde plus je me rend compte que ce pojet aurait carr\u00e9ment du \u00eatre en TDD (Test Driven Developement) par ce que ca m'aurait fait gagner un temps FOU. OK JE SUIS DEBILE POURQUOI J'AI PAS FAIT CA PLUS T\u00d4T ??? En fait ce que j'aurais du faire c'est prendre de gros \u00e9chantillons de toutes les types de windows et j'aurais un parfait framework pour savoir si j'ai am\u00e9lior\u00e9 mon OCR ou non. Voici les exemples que je vais utiliser pour verifier le bon fonctionnement de l'OCR : \"Dataset Ecarts\" ; \"Dataset Tours\" ; \"Dataset Noms\" ; \"Dataset Positions\" ; \"Dataset Sectors\" ; \"Dataset Tyres\" ; Je pense que c'est un set assez correct car j'ai essay\u00e9 de prendre un peu tous les cas possibles. Le seul qui m'inqui\u00e8te un peu c'est celui des pneus mais bon. C'est aussi celui qui m'inqui\u00e8te le plus en temps normal. Non mais c'est juste g\u00e9nial les tests en fait... j'avais pas vu que parfois ma detection de GAP TO LEADER comprenait le \"+1:34.567\" en \"61:34.567\" car le '+' \u00e9tait interpr\u00eat\u00e9 comme un 6. Sans les tests je ne m'en serais pas rendu compte. Ce qui est g\u00e9nial c'est que ca veut dire que si je veux am\u00e9liorer mon OCR j'ai juste \u00e0 mettre plus d'exemples dans le dossier de tests et de run les tests et voir ou il a des soucis. C'est un peu tard mais ca m'aurait fait gagner TELLEMENT de temps c'est absolument ridicule.","title":"Jeudi 1 Juin 2023"},{"location":"jdb.html#vendredi-2-juin-2023","text":"Aujourd'hui c'est de nouveau doc. Mais la je pense qu'il faut que je rajoute une rubrique \"Fonctionnement g\u00e9n\u00e9ral\" qui r\u00e9sume tr\u00e8s simplement toutes les \u00e9tapes du projet avec un bon diagramme. En fait c'est beaucoup plus dur que ce que je pensais de faire un bon diagramme qui explique tout sans \u00eatre illisible. J'ai mis une heure trente \u00e0 faire celui la : \"Diagramme fonctionnement g\u00e9n\u00e9ral\" J'ai pris pas mal de temps aussi \u00e0 faire ces trois autre diagrammes un peu plus graphiques qui montrent de mani\u00e8re un poil plus abstraite le fonctionnement des trois grosses parties du projet. \"Diagramme recup\u00e9ration d'images\" \"Diagramme OCR\" \"Diagramme Traitement\" Je pense qu'ils sont un peu plus faciles \u00e0 comprendre que le gros diagramme g\u00e9n\u00e9ral. Ce que je me dis c'est que je vais faire une section fonctionnement g\u00e9n\u00e9ral ou je vais expliquer les trois parties avec les petits diagramme et r\u00e9sumer avec le grand diagramme.","title":"Vendredi 2 Juin 2023"},{"location":"jdb.html#lundi-5-juin-2023","text":"Ce weekend j'ai voulu essayer le projet pour le Grand Prix de Barcelone qui s'est sold\u00e9 en un \u00e9chec cuisant... J'ai eu trois soucis pour l'utilisation du programme. Les voici dans l'ordre croissant d'importance et de difficult\u00e9 \u00e0 r\u00e8gler : Mon Laptop est incapable de rester plus d'une heure allum\u00e9 m\u00eame compl\u00eatement charg\u00e9 \u00e0 100% Le WIFI de chez moi a du mal \u00e0 g\u00e8rer plusieurs flux 1080p en m\u00eame temps et donc le temps de chargement est trop long ce qui me fait des erreurs 105 \"Ce que voyait le programme pendant les erreurs 105 ce dimanche\" Le fait que quand un flux est en live, quand on clique dessus, un nouveau bouton appar\u00e2it qui n'\u00e9tait pas pr\u00e9vu et qui nous propose de regarder depuis le d\u00e9but ou en live. Le soucis avec ce bouton c'est qu'il n'est pr\u00e9sent que pour les sessions live... Il va donc falloir attendre le prochain GP (Canada 18 Juin) pour faire un test en conditions r\u00e9elles. \"Ce que voyait le programme avant que j'essaie d'impl\u00e9menter le click auto\" J'ai essay\u00e9 de r\u00e8gler le probl\u00e8me en Live, le soucis c'est que j'ai un commentaire \u00e0 fournir pendant la course et donc je n'ai pas eu le temps de plus me pencher sur le cas. Le weekend du prochain Grand Prix je vais essayer de tester sur les sc\u00e9ances d'essais libres.le click du bouton et j'amenerai mon cable chez moi. Bon sinon aujourd'hui, comme d'hab, Doc... Je vais faire le manuel utilisateur. J'ai fait un manuel qui d\u00e9crit \u00e0 peu pr\u00e8s tout ce qui'il faut savoir pour bien utiliser l'app. Cela fait un document un peu long mais je pense que c'est nescessaire car c'est vraiment pas une app facile \u00e0 comprendre quand on ne vient pas du milieu de la F1 (et m\u00eame la...)","title":"Lundi 5 Juin 2023"},{"location":"jdb.html#mardi-6-juin-2023","text":"Aujourd'hui je vais continuer \u00e0 documenter... Ma methode pour l'instant c'est juste de remplir les titres que j'ai pr\u00e9vu au d\u00e9part. Ensuite \u00e0 partir de jeudi (je pense que c'est \u00e0 partir de cette date que j'aurai un peu tout rempli) j'aimerais bien relire la grille d'\u00e9valuation et ensuite faire une lecture de mon journal de bord pour v\u00e9rifier que je n'ai rien oubli\u00e9. Et le but c'est de finir la doc Vendredi soir pour faire une derni\u00e8re release doc + projet. Un coll\u00e8gue M.Briard m'a pas mal aid\u00e9 avec la configuration de mon mkdocs et il a d\u00e9velopp\u00e9 une extension pour ajouter le code source au PDF final. La il est en train de regarder si il peut trouver un moyen de faire une table des figures qui nous est demand\u00e9e. (j'ai plus de 200 images dans ma documentation alors si c'est possible de ne pas avoir \u00e0 faire une table des figures \u00e0 la main je prends)","title":"Mardi 6 Juin 2023"},{"location":"jdb.html#mercredi-7-juin","text":"La je suis en train de parler de l'optimisation de mon application et je viens de me rappeller qu'il manquait des methodes avec de la paralellisation alors je vais les convertir avant de continuer la doc. En fait je viens de me rendre compte qu'aucunes de mes methodes de filtres n'\u00e9taient en parralel... Je ne sais pas si jaurai le temps de le faire aujourd'hui en fait Voici le code avant la paralellisation : public static Bitmap VanishOxyAction ( 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 blue = ( int ) pixel [ 0 ]; int green = ( int ) pixel [ 1 ]; int red = ( int ) pixel [ 2 ]; int max = Math . Max ( Math . Max ( blue , green ), red ); if ( max > 255 / 3 ) max = 255 ; pixel [ 0 ] = pixel [ 1 ] = pixel [ 2 ] = ( byte ) max ; } } } inputBitmap . UnlockBits ( bmpData ); return inputBitmap ; } Et voici \u00e0 quoi ca ressemble avec la paralellisation : public Bitmap VanishOxyAction ( Bitmap inputBitmap ) { unsafe { BitmapData bitmapData = inputBitmap . LockBits ( new Rectangle ( 0 , 0 , inputBitmap . Width , inputBitmap . Height ), ImageLockMode . ReadWrite , inputBitmap . PixelFormat ); int bytesPerPixel = System . Drawing . Bitmap . GetPixelFormatSize ( inputBitmap . PixelFormat ) / 8 ; int heightInPixels = bitmapData . Height ; int widthInBytes = bitmapData . Width * bytesPerPixel ; byte * PtrFirstPixel = ( byte *) bitmapData . Scan0 ; Parallel . For ( 0 , heightInPixels , y => { byte * currentLine = PtrFirstPixel + ( y * bitmapData . Stride ); for ( int x = 0 ; x < widthInBytes ; x = x + bytesPerPixel ) { int blue = currentLine [ x ]; int green = currentLine [ x + 1 ]; int red = currentLine [ x + 2 ]; int max = Math . Max ( Math . Max ( blue , green ), red ); if ( max > 255 / 3 ) max = 255 ; currentLine [ x ] = currentLine [ x + 1 ] = currentLine [ x + 2 ] = ( byte ) max ; } }); inputBitmap . UnlockBits ( bitmapData ); } return inputBitmap ; } Les performances n'ont pas beaucoup augment\u00e9 mais au moins comme ca c'est fait","title":"Mercredi 7 Juin"},{"location":"jdb.html#jeudi-8-juin-2023","text":"Aujourd'hui, le but est de finir la documentation et de mettre les derniers commentaires dans le code et faire une premi\u00e8re release en BETA. Pour ce genre de travail un peu p\u00e9nible je conseille une bonne playlist de phonk. Ca permet d'\u00e9crire en rythme.","title":"Jeudi 8 Juin 2023"},{"location":"Code/ConfigurationTool.html","text":"ConfigurationTool.cs /// Author : Maxime Rohmer /// Date : 30/05/2023 /// File : ConfigurationTool.cs /// Brief : Class that contains all the methods used to create config files for the main programm /// Version : Alpha 1.0 using System ; using System.Collections.Generic ; using System.Drawing ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using Tesseract ; using System.IO ; using System.Text.Json ; using System.Text.Json.Nodes ; namespace TrackTrends { 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/\" ; /// <summary> /// Creates the configuration tool. It can only be created if you already have the dimensions of the main zone /// </summary> /// <param name=\"fullImage\">The full image coming from the F1TV Data Channel</param> /// <param name=\"mainZoneDimensions\">The dimensions of the zone where all the drivers data are situated</param> public ConfigurationTool ( Bitmap fullImage , Rectangle mainZoneDimensions ) { MainZone = new Zone ( fullImage , mainZoneDimensions , \"Main\" ); AutoCalibrate (); } /// <summary> /// Resets the main zone /// </summary> public void ResetMainZone () { MainZone . ResetZones (); } /// <summary> /// Reset the windows /// </summary> public void ResetWindows () { MainZone . ResetWindows (); } /// <summary> /// Save the current config in a JSON file stored in /Presets/ /// </summary> /// <param name=\"drivers\">A list of all the drivers in the GP. IMPORTANT, they need to ALL be mentionned or the program wont be able to detect the missing ones and will F up everything</param> /// <param name=\"configName\">The name the config should have</param> public void SaveToJson ( List < string > drivers , string configName ) { string JSON = \"\" ; JsonObject jsonFileObject = new JsonObject (); //Creates the mainZone object JsonObject mainZoneObject = new JsonObject (); mainZoneObject . Add ( \"x\" , MainZone . Bounds . X ); mainZoneObject . Add ( \"y\" , MainZone . Bounds . Y ); mainZoneObject . Add ( \"width\" , MainZone . Bounds . Width ); mainZoneObject . Add ( \"height\" , MainZone . Bounds . Height ); JsonArray driverZonesArray = new JsonArray (); //Creates all the subzones that contain driver infos int DriverID = 0 ; foreach ( Zone driverZone in MainZone . Zones ) { DriverID ++; JsonObject driverZoneObject = new JsonObject (); driverZoneObject . Add ( \"name\" , \"Driver\" + DriverID ); driverZoneObject . Add ( \"x\" , driverZone . Bounds . X ); driverZoneObject . Add ( \"y\" , driverZone . Bounds . Y ); driverZoneObject . Add ( \"width\" , driverZone . Bounds . Width ); driverZoneObject . Add ( \"height\" , driverZone . Bounds . Height ); JsonArray windowsArray = new JsonArray (); JsonObject windowObject = new JsonObject (); //Creates all the windows of the current driver zone //Note : We store ALL the windows and zones in the JSON because they are not spaced exactly the same on the main zone foreach ( Window window in driverZone . Windows ) { windowObject . Add ( window . Name , new JsonObject { { \"x\" , window . Bounds . X }, { \"y\" , window . Bounds . Y }, { \"width\" , window . Bounds . Width }, { \"height\" , window . Bounds . Height } }); } windowsArray . Add ( windowObject ); driverZoneObject . Add ( \"Windows\" , windowsArray ); driverZonesArray . Add ( driverZoneObject ); } mainZoneObject . Add ( \"DriverZones\" , driverZonesArray ); JsonArray driversArray = new JsonArray (); foreach ( string driver in drivers ) { driversArray . Add ( driver ); } mainZoneObject . Add ( \"Drivers\" , driversArray ); jsonFileObject . Add ( \"Main\" , mainZoneObject ); JSON = jsonFileObject . ToString (); //Saving the file string path = CONFIGS_FOLDER_NAME + configName ; if ( File . Exists ( path + \".json\" )) { //We need to create a new name int count = 2 ; while ( File . Exists ( path + \"_\" + count + \".json\" )) { count ++; } path += \"_\" + count + \".json\" ; } else { path += \".json\" ; } File . WriteAllText ( path , JSON ); } /// <summary> /// Adds a window in the windows list /// Be carefull of the order. It cant be random or it will crash. The programm expect the first to be position, second Gap to leader etc... /// </summary> /// <param name=\"rectangles\">The bounds of the window</param> 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 : //Second zone should be the Gap to leader driverZone . AddWindow ( new DriverGapToLeaderWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], false )); break ; case 3 : //Third zone should be the driver's Lap Time driverZone . AddWindow ( new DriverLapTimeWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], false )); break ; case 4 : //Fourth zone should be the driver's DRS status driverZone . AddWindow ( new DriverDrsWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], false )); break ; case 5 : //Fifth zone should be the driver's Tyre's informations driverZone . AddWindow ( new DriverTyresWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], false )); break ; case 6 : //Sixth zone should be the driver's Name driverZone . AddWindow ( new DriverNameWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], false )); break ; case 7 : //Seventh zone should be the driver's First Sector driverZone . AddWindow ( new DriverSectorWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], 1 , false )); break ; case 8 : //Zone number eight should be the driver's Second Sector driverZone . AddWindow ( new DriverSectorWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], 2 , false )); break ; case 9 : //Zone number nine should be the driver's Position Sector driverZone . AddWindow ( new DriverSectorWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], 3 , false )); break ; } } } } /// <summary> /// This will automatically create all the driver zones at the correct places if the main zone has been weel positionned /// You cant just divide the image by the number of pilots or it will be messy and inconsistent at the end (Garbage in Garbage Out) /// </summary> 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 ); //Runs a quick OCR detection. Not to detect any content but just to detect where is all the text positionned. //For each row it decides the best Zone location and adds it to the Driver zone list using ( var iter = page . GetIterator ()) { iter . Begin (); do { Rect boundingBox ; if ( iter . TryGetBoundingBox ( PageIteratorLevel . Word , out boundingBox )) { //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":"/// Author : Maxime Rohmer /// Date : 30/05/2023 /// File : ConfigurationTool.cs /// Brief : Class that contains all the methods used to create config files for the main programm /// Version : Alpha 1.0 using System ; using System.Collections.Generic ; using System.Drawing ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using Tesseract ; using System.IO ; using System.Text.Json ; using System.Text.Json.Nodes ; namespace TrackTrends { 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/\" ; /// <summary> /// Creates the configuration tool. It can only be created if you already have the dimensions of the main zone /// </summary> /// <param name=\"fullImage\">The full image coming from the F1TV Data Channel</param> /// <param name=\"mainZoneDimensions\">The dimensions of the zone where all the drivers data are situated</param> public ConfigurationTool ( Bitmap fullImage , Rectangle mainZoneDimensions ) { MainZone = new Zone ( fullImage , mainZoneDimensions , \"Main\" ); AutoCalibrate (); } /// <summary> /// Resets the main zone /// </summary> public void ResetMainZone () { MainZone . ResetZones (); } /// <summary> /// Reset the windows /// </summary> public void ResetWindows () { MainZone . ResetWindows (); } /// <summary> /// Save the current config in a JSON file stored in /Presets/ /// </summary> /// <param name=\"drivers\">A list of all the drivers in the GP. IMPORTANT, they need to ALL be mentionned or the program wont be able to detect the missing ones and will F up everything</param> /// <param name=\"configName\">The name the config should have</param> public void SaveToJson ( List < string > drivers , string configName ) { string JSON = \"\" ; JsonObject jsonFileObject = new JsonObject (); //Creates the mainZone object JsonObject mainZoneObject = new JsonObject (); mainZoneObject . Add ( \"x\" , MainZone . Bounds . X ); mainZoneObject . Add ( \"y\" , MainZone . Bounds . Y ); mainZoneObject . Add ( \"width\" , MainZone . Bounds . Width ); mainZoneObject . Add ( \"height\" , MainZone . Bounds . Height ); JsonArray driverZonesArray = new JsonArray (); //Creates all the subzones that contain driver infos int DriverID = 0 ; foreach ( Zone driverZone in MainZone . Zones ) { DriverID ++; JsonObject driverZoneObject = new JsonObject (); driverZoneObject . Add ( \"name\" , \"Driver\" + DriverID ); driverZoneObject . Add ( \"x\" , driverZone . Bounds . X ); driverZoneObject . Add ( \"y\" , driverZone . Bounds . Y ); driverZoneObject . Add ( \"width\" , driverZone . Bounds . Width ); driverZoneObject . Add ( \"height\" , driverZone . Bounds . Height ); JsonArray windowsArray = new JsonArray (); JsonObject windowObject = new JsonObject (); //Creates all the windows of the current driver zone //Note : We store ALL the windows and zones in the JSON because they are not spaced exactly the same on the main zone foreach ( Window window in driverZone . Windows ) { windowObject . Add ( window . Name , new JsonObject { { \"x\" , window . Bounds . X }, { \"y\" , window . Bounds . Y }, { \"width\" , window . Bounds . Width }, { \"height\" , window . Bounds . Height } }); } windowsArray . Add ( windowObject ); driverZoneObject . Add ( \"Windows\" , windowsArray ); driverZonesArray . Add ( driverZoneObject ); } mainZoneObject . Add ( \"DriverZones\" , driverZonesArray ); JsonArray driversArray = new JsonArray (); foreach ( string driver in drivers ) { driversArray . Add ( driver ); } mainZoneObject . Add ( \"Drivers\" , driversArray ); jsonFileObject . Add ( \"Main\" , mainZoneObject ); JSON = jsonFileObject . ToString (); //Saving the file string path = CONFIGS_FOLDER_NAME + configName ; if ( File . Exists ( path + \".json\" )) { //We need to create a new name int count = 2 ; while ( File . Exists ( path + \"_\" + count + \".json\" )) { count ++; } path += \"_\" + count + \".json\" ; } else { path += \".json\" ; } File . WriteAllText ( path , JSON ); } /// <summary> /// Adds a window in the windows list /// Be carefull of the order. It cant be random or it will crash. The programm expect the first to be position, second Gap to leader etc... /// </summary> /// <param name=\"rectangles\">The bounds of the window</param> 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 : //Second zone should be the Gap to leader driverZone . AddWindow ( new DriverGapToLeaderWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], false )); break ; case 3 : //Third zone should be the driver's Lap Time driverZone . AddWindow ( new DriverLapTimeWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], false )); break ; case 4 : //Fourth zone should be the driver's DRS status driverZone . AddWindow ( new DriverDrsWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], false )); break ; case 5 : //Fifth zone should be the driver's Tyre's informations driverZone . AddWindow ( new DriverTyresWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], false )); break ; case 6 : //Sixth zone should be the driver's Name driverZone . AddWindow ( new DriverNameWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], false )); break ; case 7 : //Seventh zone should be the driver's First Sector driverZone . AddWindow ( new DriverSectorWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], 1 , false )); break ; case 8 : //Zone number eight should be the driver's Second Sector driverZone . AddWindow ( new DriverSectorWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], 2 , false )); break ; case 9 : //Zone number nine should be the driver's Position Sector driverZone . AddWindow ( new DriverSectorWindow ( driverZone . ZoneImage , rectangles [ i - 1 ], 3 , false )); break ; } } } } /// <summary> /// This will automatically create all the driver zones at the correct places if the main zone has been weel positionned /// You cant just divide the image by the number of pilots or it will be messy and inconsistent at the end (Garbage in Garbage Out) /// </summary> 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 ); //Runs a quick OCR detection. Not to detect any content but just to detect where is all the text positionned. //For each row it decides the best Zone location and adds it to the Driver zone list using ( var iter = page . GetIterator ()) { iter . Begin (); do { Rect boundingBox ; if ( iter . TryGetBoundingBox ( PageIteratorLevel . Word , out boundingBox )) { //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/DriverData.html","text":"DriverData.cs /// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DriverData.cs /// Brief : File containing classes that behave just like structures to store data about drivers /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; namespace TrackTrends { 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 ; } /// <summary> /// Creates a default driver data with empty values /// </summary> 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":"/// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DriverData.cs /// Brief : File containing classes that behave just like structures to store data about drivers /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; namespace TrackTrends { 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 ; } /// <summary> /// Creates a default driver data with empty values /// </summary> 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/DriverGapToLeaderWindow.html","text":"DriverGapToLeaderWindow.cs /// Author : Maxime Rohmer /// Date : 30/05/2023 /// File : DriverGapToLeaderWindow.cs /// Brief : Window containing infos about the gap to the leader of a driver /// Version : Alpha 1.0 using System ; using System.Collections.Generic ; using System.Drawing ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; namespace TrackTrends { public 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 the gap to the leader in miliseconds (int)</returns> public override object DecodePng () { int result = GetTimeFromPng ( WindowImage , OcrImage . WindowType . Gap , Engine ); return result ; } } }","title":"DriverGapToLeaderWindow.cs"},{"location":"Code/DriverGapToLeaderWindow.html#drivergaptoleaderwindowcs","text":"/// Author : Maxime Rohmer /// Date : 30/05/2023 /// File : DriverGapToLeaderWindow.cs /// Brief : Window containing infos about the gap to the leader of a driver /// Version : Alpha 1.0 using System ; using System.Collections.Generic ; using System.Drawing ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; namespace TrackTrends { public 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 the gap to the leader in miliseconds (int)</returns> public override object DecodePng () { int result = GetTimeFromPng ( WindowImage , OcrImage . WindowType . Gap , Engine ); return result ; } } }","title":"DriverGapToLeaderWindow.cs"},{"location":"Code/DriverNameWindow.html","text":"DriverNameWindow.cs /// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DriverNameWindow /// Brief : Window containing infos about the name of the driver /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using System.Drawing ; namespace TrackTrends { public class DriverNameWindow : Window { 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\">A list of all the names that can be on the image</param> /// <returns>a string representing the found driver name. It will be one of the ones given in the list</returns> public override object DecodePng ( List < string > DriverList ) { string result = \"\" ; result = 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\">The list of all the drivers name that can be found in the image</param> /// <param name=\"potentialDriver\">The driver you want to be sure if it exists or not</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":"/// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DriverNameWindow /// Brief : Window containing infos about the name of the driver /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using System.Drawing ; namespace TrackTrends { public class DriverNameWindow : Window { 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\">A list of all the names that can be on the image</param> /// <returns>a string representing the found driver name. It will be one of the ones given in the list</returns> public override object DecodePng ( List < string > DriverList ) { string result = \"\" ; result = 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\">The list of all the drivers name that can be found in the image</param> /// <param name=\"potentialDriver\">The driver you want to be sure if it exists or not</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/DriverSectorWindow.html","text":"DriverSectorWindow.cs /// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DriverSectorWindow.cs /// Brief : Window containing infos about a driver sector time. Can be the first second or third, does not matter. /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using System.Drawing ; namespace TrackTrends { public 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 object DecodePng () { int ocrResult = GetTimeFromPng ( WindowImage , OcrImage . WindowType . Sector , Engine ); return ocrResult ; } } }","title":"DriverSectorWindow.cs"},{"location":"Code/DriverSectorWindow.html#driversectorwindowcs","text":"/// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DriverSectorWindow.cs /// Brief : Window containing infos about a driver sector time. Can be the first second or third, does not matter. /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using System.Drawing ; namespace TrackTrends { public 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 object DecodePng () { int ocrResult = GetTimeFromPng ( WindowImage , OcrImage . WindowType . Sector , Engine ); return ocrResult ; } } }","title":"DriverSectorWindow.cs"},{"location":"Code/F1TVEmulator.html","text":"F1TVEmulator.cs /// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : F1TVEmulator.cs /// Brief : Class that contains methods to emulate a browser and navigate the F1TV website /// Version : Beta 1.0 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 TrackTrends { 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 ; } /// <summary> /// Will start the python programm that runs the Cookie Recovering /// </summary> 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 (); } /// <summary> /// Method that will recover the needed cookies in the DB /// </summary> /// <param name=\"host\"> The host of the wanted cookie ex: ./formula1.com</param> /// <param name=\"name\">The name of the wanted cookie ex: login</param> /// <returns>returns the value of the cookie if it has been found</returns> /// <exception cref=\"InvalidOperationException\"></exception> public string GetCookie ( string host , string name ) { StartCookieRecovering (); string value = \"\" ; List < Cookie > cookies = new List < Cookie >(); if ( File . Exists ( COOKIES_CSV_FILENAME )) { 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 ; } /// <summary> /// Starts the headless browser /// </summary> /// <returns>Error code 1xx</returns> 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 ); //Cookie retreival has gone wrong (usually its because of python not being installed properly) if ( loginCookieValue == \"\" || loginSessionValue == \"\" ) return 100 ; 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 ; } try { IWebElement LiveButton = Driver . FindElement ( By . ClassName ( \"btn-manage-account\" )); //By.ClassName(\"btn btn-controls btn-main btn-manage-account no-redirect\") LiveButton . Click (); } catch { Console . Write ( \"Going for a rediff\" ); } //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 settingsClickTries = 0 ; bool settingsClickSuccess = false ; while ( settingsClickTries < 100 && ! settingsClickSuccess ) { 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 (); settingsClickSuccess = true ; } catch { //Sometimes it can crash because it could not get the options to show up in time. When it happens just retry settingsClickSuccess = false ; settingsClickTries ++; } } if (! settingsClickSuccess ) { Screenshot ( \"ERROR105\" ); Driver . Dispose (); return 105 ; } Screenshot ( \"BEFOREFULLSCREEN\" ); //Makes the feed fullscreen int fullScreenClickTries = 0 ; bool fullScreenClickSuccess = false ; Driver . Manage (). Window . Maximize (); //WebDriverWait wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); while ( fullScreenClickTries < 100 && ! fullScreenClickSuccess ) { Thread . Sleep ( 150 ); try { IWebElement fullScreenButton = Driver . FindElement ( By . ClassName ( \"bmpui-ui-fullscreentogglebutton\" )); fullScreenButton . Click (); fullScreenClickSuccess = true ; } catch { fullScreenClickSuccess = false ; fullScreenClickTries ++; } } if (! fullScreenClickSuccess ) { Screenshot ( \"ERROR106\" ); Driver . Dispose (); return 106 ; } Screenshot ( \"AFTERFULLSCREEN\" ); //STARTUP FINISHED READY TO SCREENSHOT Ready = true ; return 0 ; } /// <summary> /// Takes a screenshot of what the headless browser is displaying /// </summary> /// <param name=\"name\">Optional ! The name of the picture so it can be saved</param> /// <returns>Returns the screenshot in the bitmap format</returns> public Bitmap Screenshot ( string name = \"TEST\" ) { Bitmap result = new Bitmap ( 4242 , 6969 ); try { //Screenshot scrsht = ((ITakesScreenshot)Driver).GetScreenshot(); //profileriver.SetPreferencC:\\Users\\Moi\\source\\repos\\Test_Merge\\README.mde(\"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 ; } /// <summary> /// Stops the Emulation. Note: if you plan to start it again please use ResetDriver() instead /// </summary> public void Stop () { Ready = false ; if ( Driver != null ) Driver . Dispose (); } /// <summary> /// Resets the emulation /// </summary> public void ResetDriver () { Ready = false ; if ( Driver != null ) Driver . Dispose (); Driver = null ; } } }","title":"F1TVEmulator.cs"},{"location":"Code/F1TVEmulator.html#f1tvemulatorcs","text":"/// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : F1TVEmulator.cs /// Brief : Class that contains methods to emulate a browser and navigate the F1TV website /// Version : Beta 1.0 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 TrackTrends { 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 ; } /// <summary> /// Will start the python programm that runs the Cookie Recovering /// </summary> 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 (); } /// <summary> /// Method that will recover the needed cookies in the DB /// </summary> /// <param name=\"host\"> The host of the wanted cookie ex: ./formula1.com</param> /// <param name=\"name\">The name of the wanted cookie ex: login</param> /// <returns>returns the value of the cookie if it has been found</returns> /// <exception cref=\"InvalidOperationException\"></exception> public string GetCookie ( string host , string name ) { StartCookieRecovering (); string value = \"\" ; List < Cookie > cookies = new List < Cookie >(); if ( File . Exists ( COOKIES_CSV_FILENAME )) { 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 ; } /// <summary> /// Starts the headless browser /// </summary> /// <returns>Error code 1xx</returns> 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 ); //Cookie retreival has gone wrong (usually its because of python not being installed properly) if ( loginCookieValue == \"\" || loginSessionValue == \"\" ) return 100 ; 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 ; } try { IWebElement LiveButton = Driver . FindElement ( By . ClassName ( \"btn-manage-account\" )); //By.ClassName(\"btn btn-controls btn-main btn-manage-account no-redirect\") LiveButton . Click (); } catch { Console . Write ( \"Going for a rediff\" ); } //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 settingsClickTries = 0 ; bool settingsClickSuccess = false ; while ( settingsClickTries < 100 && ! settingsClickSuccess ) { 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 (); settingsClickSuccess = true ; } catch { //Sometimes it can crash because it could not get the options to show up in time. When it happens just retry settingsClickSuccess = false ; settingsClickTries ++; } } if (! settingsClickSuccess ) { Screenshot ( \"ERROR105\" ); Driver . Dispose (); return 105 ; } Screenshot ( \"BEFOREFULLSCREEN\" ); //Makes the feed fullscreen int fullScreenClickTries = 0 ; bool fullScreenClickSuccess = false ; Driver . Manage (). Window . Maximize (); //WebDriverWait wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); while ( fullScreenClickTries < 100 && ! fullScreenClickSuccess ) { Thread . Sleep ( 150 ); try { IWebElement fullScreenButton = Driver . FindElement ( By . ClassName ( \"bmpui-ui-fullscreentogglebutton\" )); fullScreenButton . Click (); fullScreenClickSuccess = true ; } catch { fullScreenClickSuccess = false ; fullScreenClickTries ++; } } if (! fullScreenClickSuccess ) { Screenshot ( \"ERROR106\" ); Driver . Dispose (); return 106 ; } Screenshot ( \"AFTERFULLSCREEN\" ); //STARTUP FINISHED READY TO SCREENSHOT Ready = true ; return 0 ; } /// <summary> /// Takes a screenshot of what the headless browser is displaying /// </summary> /// <param name=\"name\">Optional ! The name of the picture so it can be saved</param> /// <returns>Returns the screenshot in the bitmap format</returns> public Bitmap Screenshot ( string name = \"TEST\" ) { Bitmap result = new Bitmap ( 4242 , 6969 ); try { //Screenshot scrsht = ((ITakesScreenshot)Driver).GetScreenshot(); //profileriver.SetPreferencC:\\Users\\Moi\\source\\repos\\Test_Merge\\README.mde(\"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 ; } /// <summary> /// Stops the Emulation. Note: if you plan to start it again please use ResetDriver() instead /// </summary> public void Stop () { Ready = false ; if ( Driver != null ) Driver . Dispose (); } /// <summary> /// Resets the emulation /// </summary> public void ResetDriver () { Ready = false ; if ( Driver != null ) Driver . Dispose (); Driver = null ; } } }","title":"F1TVEmulator.cs"},{"location":"Code/Form1.html","text":"Form1.cs /// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : Form1.cs /// Brief : Class that controls the main view of the app /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.ComponentModel ; using System.Data ; using System.Diagnostics ; using System.Drawing ; using System.Linq ; using System.Text ; using System.Threading ; using System.Threading.Tasks ; using System.Windows.Forms ; using System.IO ; namespace TrackTrends { public partial class Main : Form { private F1TVEmulator Emulator = null ; private DataWrapper Wrapper = null ; private bool cancelRequested = false ; private SemaphoreSlim semaphore = new SemaphoreSlim ( 1 ); string ConfigFile = \"\" ; string GpUrl = \"\" ; //For the responsive content Size oldSize = new Size (); Size oldRankingSize = new Size (); Size oldLapTimesSize = new Size (); Size oldBattles = new Size (); Size oldPnlBattles = new Size (); Size oldPnlRankings = new Size (); Size oldPnlFastest = new Size (); Size oldPnlSlowest = new Size (); Point oldRankingPosition = new Point (); Point oldBattlePosition = new Point (); Point oldDriverInfoPosition = new Point (); Point olPnlFastestPosition = new Point (); Point oldPnlSlowestPosition = new Point (); public Main () { InitializeComponent (); } /// <summary> /// Will update everything that is not data related /// </summary> public void RefreshUI () { if ( Directory . Exists ( ConfigurationTool . CONFIGS_FOLDER_NAME )) { lsbPresets . DataSource = null ; lsbPresets . DataSource = Directory . GetFiles ( ConfigurationTool . CONFIGS_FOLDER_NAME ); } } /// <summary> /// Opens the settings page. Also disposes of the browser if there is one opened and all thos things /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnSettings_Click ( object sender , EventArgs e ) { if ( Emulator != null ) Emulator . ResetDriver (); btnStartDecoding . Enabled = false ; btnStopUpdating . Enabled = false ; btnResetEmulator . Text = \"Launch\" ; Emulator = null ; Wrapper = null ; GC . Collect (); Settings settingsForm = new Settings (); settingsForm . ShowDialog (); RefreshUI (); //MessageBox.Show(settingsForm.GrandPrixUrl + Environment.NewLine + settingsForm.GrandPrixName + Environment.NewLine + settingsForm.GrandPrixYear); if ( settingsForm . GrandPrixUrl != \"\" && settingsForm . SelectedConfigFile != \"\" ) { GpUrl = settingsForm . GrandPrixUrl ; tbxGpUrl . Text = GpUrl ; if ( File . Exists ( settingsForm . SelectedConfigFile )) { ConfigFile = settingsForm . SelectedConfigFile ; for ( int i = 0 ; i < lsbPresets . Items . Count ; i ++) { if ( lsbPresets . Items [ i ]. ToString () == ConfigFile ) lsbPresets . SelectedIndex = i ; } } else { //Should technically never show up but we never know MessageBox . Show ( \"The config file has not been found please return to the config and change it\" ); } } else { //WE dont care anymore, the user will choose its Grand Prix himself in the main program //MessageBox.Show(\"There is no URL for the Grand Prix you want to decode. Please return to the config and add a valid one\"); } } /// <summary> /// Will do everything that needs to be done at the first start of the app /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private async void Form1_Load ( object sender , EventArgs e ) { //Those are the default values but they will need to be changed later when the configuration has been done ConfigFile = \"./Presets/Clean_4K_2023.json\" ; GpUrl = \"https://f1tv.formula1.com/detail/1000006688/2023-azerbaijan-grand-prix?action=play\" ; tbxGpUrl . Text = GpUrl ; this . DoubleBuffered = true ; oldSize = this . Size ; oldRankingSize = gpbxRanking . Size ; oldLapTimesSize = gpbxLapTimes . Size ; oldBattles = gpbxBattles . Size ; oldPnlRankings = pnlLiveRanking . Size ; oldPnlBattles = pnlBattles . Size ; oldPnlFastest = pnlFastest . Size ; oldPnlSlowest = pnlSlowest . Size ; oldRankingPosition = gpbxRanking . Location ; oldBattlePosition = gpbxBattles . Location ; oldDriverInfoPosition = gpbxDriverInfos . Location ; olPnlFastestPosition = pnlFastest . Location ; oldPnlSlowestPosition = pnlSlowest . Location ; tip1 . SetToolTip ( btnResetEmulator , \"Starts or restarts the emulator. You need to start this to use the app\" ); tip1 . SetToolTip ( btnSettings , \"Opens the configuration menu\" ); tip1 . SetToolTip ( tbxGpUrl , \"Insert the URL of the Grand Prix you want to track. Dont forget the \\\"?action=play\\\" at the end\" ); tip1 . SetToolTip ( lsbPresets , \"Select a configuration preset to use with the decoding\" ); tip1 . SetToolTip ( pbxResult , \"A preview of what the program sees. You should see the DATA page of the F1TV here\" ); tip1 . SetToolTip ( lsbOvertakes , \"A list of all the activity. You can scroll to see the most recent overtakes\" ); tip1 . SetToolTip ( gpbxBattles , \"The four first battles in the field. A battle is two drivers less than 3 seconds apart\" ); tip1 . SetToolTip ( gpbxLapTimes , \"The fastest and slowest drivers on track at the moment. It takes the average lapTime of the last 5 laps to choose who is the fastes or the slowest\" ); RefreshUI (); } /// <summary> /// Will start or stop the process of decoding /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private async void btnUpdate_Click ( object sender , EventArgs e ) { cancelRequested = false ; if ( Emulator != null && Wrapper != null ) { // Disable UI controls to prevent re-entrancy btnResetEmulator . Enabled = false ; btnStartDecoding . Enabled = false ; btnStopUpdating . Enabled = true ; btnSettings . Enabled = false ; while (! cancelRequested ) { await semaphore . WaitAsync (); try { // Start the time-consuming task on a separate thread await Task . Run ( async () => { Stopwatch sw = new Stopwatch (); sw . Start (); Bitmap screen = Emulator . Screenshot (); screen . Save ( \"HopefullyDataScreenshot.png\" ); Invoke (( MethodInvoker ) delegate { pbxResult . Image = ( Bitmap ) screen . Clone (); }); Wrapper . ChangeImage ( screen ); int errorCode = Wrapper . Refresh (); sw . Stop (); // Task completed Invoke (( MethodInvoker ) delegate { DisplayResults ( errorCode , sw , screen ); DisplayBattles (); DisplayDeltas (); DisplayOvertakes (); }); }); } finally { semaphore . Release (); } } // Re-enable UI controls btnStopUpdating . Text = \"Stop\" ; btnStartDecoding . Enabled = true ; btnStopUpdating . Enabled = false ; btnResetEmulator . Enabled = true ; btnSettings . Enabled = true ; } } /// <summary> /// Will display the overtakes in the overtakes list box /// </summary> private void DisplayOvertakes () { Wrapper . DisplayOvertakes ( lsbOvertakes ); } /// <summary> /// Will display the battles in the battles pannel /// </summary> private void DisplayBattles () { Wrapper . DisplayBattles ( pnlBattles , this ); } /// <summary> /// Will display the time differences in the faster and slowest pannels /// </summary> private void DisplayDeltas () { Wrapper . DisplayTimesDeltas ( pnlFastest , pnlSlowest , this ); } /// <summary> /// Will try to stop the emulator (usually does not work please do not count on it) /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void Form1_FormClosing ( object sender , FormClosingEventArgs e ) { if ( Emulator != null ) { Emulator . Stop (); } } /// <summary> /// Will display the live ranking on the live ranking pannel. Its called like this because historically it was the method that just recovered the bare results from the OCR /// </summary> /// <param name=\"errorCode\"></param> /// <param name=\"sw\"></param> /// <param name=\"screen\"></param> private void DisplayResults ( int errorCode , Stopwatch sw , Bitmap screen ) { if ( errorCode != 0 ) { cancelRequested = true ; MessageBox . Show ( \"An error has occured while trying to recover data from live feed. This can happen sometimes. I would advise you to restart a few times. If the problem persists check your configuration.\" ); } else { Wrapper . DisplayLiveRanking ( pnlLiveRanking , this ); } } /// <summary> /// Will stop the data recovering operation and resets some buttons and text /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnStopUpdating_Click ( object sender , EventArgs e ) { // Set the cancellation flag cancelRequested = true ; btnStopUpdating . Enabled = false ; btnResetEmulator . Enabled = false ; btnStopUpdating . Text = \"Stopping\" ; } /// <summary> /// Will start the F1TVEmulator, again this name is historical because back at the start of this project this button did not have a name /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private async void button1_Click ( object sender , EventArgs e ) { lsbOvertakes . Items . Clear (); btnResetEmulator . Text = \"Launching\" ; btnResetEmulator . Enabled = false ; btnSettings . Enabled = true ; btnStartDecoding . Enabled = false ; btnStopUpdating . Enabled = false ; btnSettings . Enabled = false ; int errorCode = - 1 ; await Task . Run ( async () => { if ( Emulator != null ) Emulator . ResetDriver (); Emulator = null ; Wrapper = null ; GC . Collect (); Emulator = new F1TVEmulator ( GpUrl ); errorCode = await Emulator . Start (); }); if ( errorCode != 0 ) { string message = \"\" ; switch ( errorCode ) { case 100 : message = \"Error \" + errorCode + \" Could not recover cookies. It could be because of an improper installation of python or bad cookies in the chrome database. Please try to log on to the F1TV using chrome again\" ; break ; 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 ); btnResetEmulator . Enabled = true ; btnSettings . Enabled = true ; btnResetEmulator . Text = \"Retry\" ; } else { Wrapper = new DataWrapper ( ConfigFile , Emulator . Screenshot ()); btnResetEmulator . Text = \"Re launch\" ; btnResetEmulator . Enabled = true ; btnSettings . Enabled = true ; btnStartDecoding . Enabled = true ; } } /// <summary> /// Silly way to remove borders from groupbox and make them look like pannels with titles /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void removeBorders ( object sender , PaintEventArgs e ) { GroupBox gpbx = ( GroupBox ) sender ; using ( Pen pen = new Pen ( gpbx . BackColor , 50 )) { e . Graphics . DrawRectangle ( pen , 0 , 0 , gpbx . Width - 1 , gpbx . Height - 1 ); e . Graphics . DrawRectangle ( pen , 0 , 0 , gpbx . Width - 1 , gpbx . Height - 1 ); } using ( var brush = new SolidBrush ( gpbx . ForeColor )) { var textPosition = new Point ( 5 , 0 ); // Adjust the X and Y values as needed e . Graphics . DrawString ( gpbx . Text , gpbx . Font , brush , textPosition ); } } /// <summary> /// Will change the preset to use when starting the emulator /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void lsbPresets_SelectedIndexChanged ( object sender , EventArgs e ) { if ( lsbPresets . SelectedIndex >= 0 ) ConfigFile = lsbPresets . Items [ lsbPresets . SelectedIndex ]. ToString (); } /// <summary> /// Will change the URL the emulator will use, historical name again /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void textBox1_TextChanged ( object sender , EventArgs e ) { if ( tbxGpUrl . Text != \"\" ) GpUrl = tbxGpUrl . Text ; } /// <summary> /// This is called by the automatically generated buttons. Its here to fill in the driver info tab whenever the user clicks on a button that contains the name of a driver /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> public void btnDriver_Click ( object sender , EventArgs e ) { //Removes the cover if ( pnlCover . Visible = true ) pnlCover . Visible = false ; //Happens when a driver button has been clicked //MessageBox.Show((sender as Button).Name + \" has been selected\"); Button btn = ( sender as Button ); string [] parts = btn . Name . Split ( '_' ); DriverData driver = Wrapper . GetFullDriverData ( parts [ 0 ], pnlCurrentDriverLapsHistory , this ); lblCurrentDriverName . Text = driver . Name ; lblCurrentDriverPosition . Text = driver . Position . ToString (); lblCurrentDriverGapToLeader . Text = Reader . ConvertMsToTime ( driver . GapToLeader ); lblCurrentDriverLapTime . Text = Reader . ConvertMsToTime ( driver . LapTime ); lblCurrentDriverTyreAge . Text = driver . CurrentTyre . NumberOfLaps . ToString (); if ( driver . DRS ) { lblCurrentDriverDRS . Text = \"Open\" ; lblCurrentDriverDRS . ForeColor = Color . FromArgb ( 0 , 164 , 46 ); } else { lblCurrentDriverDRS . Text = \"Closed\" ; lblCurrentDriverDRS . ForeColor = Color . Black ; } switch ( driver . CurrentTyre . Coumpound ) { case Tyre . Type . Undefined : lblCurrentDriverTyreType . Text = \"uuuuh...\" ; lblCurrentDriverTyreType . ForeColor = Color . Violet ; break ; case Tyre . Type . Hard : lblCurrentDriverTyreType . Text = \"Hard\" ; lblCurrentDriverTyreType . ForeColor = Color . FromArgb ( 164 , 165 , 168 ); break ; case Tyre . Type . Medium : lblCurrentDriverTyreType . Text = \"Medium\" ; lblCurrentDriverTyreType . ForeColor = Color . FromArgb ( 245 , 191 , 0 ); break ; case Tyre . Type . Soft : lblCurrentDriverTyreType . Text = \"Soft\" ; lblCurrentDriverTyreType . ForeColor = Color . FromArgb ( 255 , 0 , 0 ); break ; case Tyre . Type . Inter : lblCurrentDriverTyreType . Text = \"Intermediate\" ; lblCurrentDriverTyreType . ForeColor = Color . FromArgb ( 0 , 164 , 46 ); break ; case Tyre . Type . Wet : lblCurrentDriverTyreType . Text = \"Wet\" ; lblCurrentDriverTyreType . ForeColor = Color . FromArgb ( 39 , 96 , 166 ); break ; } } /// <summary> /// This is supposed to be called by an automatically generated button. It should be any button with a laptime info on it /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> public void btnLapTime_Click ( object sender , EventArgs e ) { //Happens when a lapTime has been clicked Button btn = sender as Button ; string [] parts = btn . Name . Split ( '_' ); Wrapper . DisplayLapTimeInfos ( parts [ 0 ], Convert . ToInt32 ( parts [ 1 ]), btn . Text ); } /// <summary> /// Will trigger responsive calculation everytime the form changes size /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void Main_Resize ( object sender , EventArgs e ) { int xDiff = this . Width - oldSize . Width ; int yDiff = this . Height - oldSize . Height ; int padding = 10 ; //This will take half the newly created space gpbxRanking . Size = new Size ( oldRankingSize . Width + xDiff / 2 , oldRankingSize . Height + yDiff ); gpbxRanking . Location = new Point ( oldRankingPosition . X + xDiff / 2 , gpbxRanking . Location . Y ); //Will take half the new height and half the new height gpbxLapTimes . Size = new Size ( oldLapTimesSize . Width + xDiff / 2 , oldLapTimesSize . Height + yDiff / 2 ); //Will take half the new height and half the new width gpbxBattles . Size = new Size ( oldBattles . Width + xDiff / 2 , oldBattles . Height + yDiff / 2 ); gpbxBattles . Location = new Point ( gpbxBattles . Location . X , oldBattlePosition . Y + yDiff / 2 ); //The infos wont change width but will need to be centerd Point startOfZone = new Point ( gpbxOvertakes . Width + gpbxOvertakes . Location . X , gpbxOvertakes . Location . Y ); Point endOfZone = new Point ( gpbxRanking . Location . X , gpbxOvertakes . Location . Y ); int totalWidth = endOfZone . X - startOfZone . X ; gpbxDriverInfos . Location = new Point ( startOfZone . X + ( totalWidth / 2 - gpbxDriverInfos . Width / 2 ), oldDriverInfoPosition . Y + yDiff ); //Now resizing internals pnlFastest . Size = new Size ( oldPnlFastest . Width + xDiff / 4 , oldPnlFastest . Height + yDiff / 4 ); pnlFastest . Location = new Point ( olPnlFastestPosition . X , olPnlFastestPosition . Y + yDiff / 4 ); pnlSlowest . Size = new Size ( oldPnlSlowest . Width + xDiff / 4 , oldPnlSlowest . Height + yDiff / 4 ); pnlSlowest . Location = new Point ( oldPnlSlowestPosition . X + xDiff / 4 , oldPnlSlowestPosition . Y + yDiff / 4 ); pnlBattles . Size = new Size ( oldPnlBattles . Width + xDiff / 2 , oldPnlBattles . Height + yDiff / 2 ); pnlLiveRanking . Size = new Size ( oldPnlRankings . Width + xDiff / 2 , oldPnlRankings . Height + yDiff ); } } }","title":"Form1.cs"},{"location":"Code/Form1.html#form1cs","text":"/// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : Form1.cs /// Brief : Class that controls the main view of the app /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.ComponentModel ; using System.Data ; using System.Diagnostics ; using System.Drawing ; using System.Linq ; using System.Text ; using System.Threading ; using System.Threading.Tasks ; using System.Windows.Forms ; using System.IO ; namespace TrackTrends { public partial class Main : Form { private F1TVEmulator Emulator = null ; private DataWrapper Wrapper = null ; private bool cancelRequested = false ; private SemaphoreSlim semaphore = new SemaphoreSlim ( 1 ); string ConfigFile = \"\" ; string GpUrl = \"\" ; //For the responsive content Size oldSize = new Size (); Size oldRankingSize = new Size (); Size oldLapTimesSize = new Size (); Size oldBattles = new Size (); Size oldPnlBattles = new Size (); Size oldPnlRankings = new Size (); Size oldPnlFastest = new Size (); Size oldPnlSlowest = new Size (); Point oldRankingPosition = new Point (); Point oldBattlePosition = new Point (); Point oldDriverInfoPosition = new Point (); Point olPnlFastestPosition = new Point (); Point oldPnlSlowestPosition = new Point (); public Main () { InitializeComponent (); } /// <summary> /// Will update everything that is not data related /// </summary> public void RefreshUI () { if ( Directory . Exists ( ConfigurationTool . CONFIGS_FOLDER_NAME )) { lsbPresets . DataSource = null ; lsbPresets . DataSource = Directory . GetFiles ( ConfigurationTool . CONFIGS_FOLDER_NAME ); } } /// <summary> /// Opens the settings page. Also disposes of the browser if there is one opened and all thos things /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnSettings_Click ( object sender , EventArgs e ) { if ( Emulator != null ) Emulator . ResetDriver (); btnStartDecoding . Enabled = false ; btnStopUpdating . Enabled = false ; btnResetEmulator . Text = \"Launch\" ; Emulator = null ; Wrapper = null ; GC . Collect (); Settings settingsForm = new Settings (); settingsForm . ShowDialog (); RefreshUI (); //MessageBox.Show(settingsForm.GrandPrixUrl + Environment.NewLine + settingsForm.GrandPrixName + Environment.NewLine + settingsForm.GrandPrixYear); if ( settingsForm . GrandPrixUrl != \"\" && settingsForm . SelectedConfigFile != \"\" ) { GpUrl = settingsForm . GrandPrixUrl ; tbxGpUrl . Text = GpUrl ; if ( File . Exists ( settingsForm . SelectedConfigFile )) { ConfigFile = settingsForm . SelectedConfigFile ; for ( int i = 0 ; i < lsbPresets . Items . Count ; i ++) { if ( lsbPresets . Items [ i ]. ToString () == ConfigFile ) lsbPresets . SelectedIndex = i ; } } else { //Should technically never show up but we never know MessageBox . Show ( \"The config file has not been found please return to the config and change it\" ); } } else { //WE dont care anymore, the user will choose its Grand Prix himself in the main program //MessageBox.Show(\"There is no URL for the Grand Prix you want to decode. Please return to the config and add a valid one\"); } } /// <summary> /// Will do everything that needs to be done at the first start of the app /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private async void Form1_Load ( object sender , EventArgs e ) { //Those are the default values but they will need to be changed later when the configuration has been done ConfigFile = \"./Presets/Clean_4K_2023.json\" ; GpUrl = \"https://f1tv.formula1.com/detail/1000006688/2023-azerbaijan-grand-prix?action=play\" ; tbxGpUrl . Text = GpUrl ; this . DoubleBuffered = true ; oldSize = this . Size ; oldRankingSize = gpbxRanking . Size ; oldLapTimesSize = gpbxLapTimes . Size ; oldBattles = gpbxBattles . Size ; oldPnlRankings = pnlLiveRanking . Size ; oldPnlBattles = pnlBattles . Size ; oldPnlFastest = pnlFastest . Size ; oldPnlSlowest = pnlSlowest . Size ; oldRankingPosition = gpbxRanking . Location ; oldBattlePosition = gpbxBattles . Location ; oldDriverInfoPosition = gpbxDriverInfos . Location ; olPnlFastestPosition = pnlFastest . Location ; oldPnlSlowestPosition = pnlSlowest . Location ; tip1 . SetToolTip ( btnResetEmulator , \"Starts or restarts the emulator. You need to start this to use the app\" ); tip1 . SetToolTip ( btnSettings , \"Opens the configuration menu\" ); tip1 . SetToolTip ( tbxGpUrl , \"Insert the URL of the Grand Prix you want to track. Dont forget the \\\"?action=play\\\" at the end\" ); tip1 . SetToolTip ( lsbPresets , \"Select a configuration preset to use with the decoding\" ); tip1 . SetToolTip ( pbxResult , \"A preview of what the program sees. You should see the DATA page of the F1TV here\" ); tip1 . SetToolTip ( lsbOvertakes , \"A list of all the activity. You can scroll to see the most recent overtakes\" ); tip1 . SetToolTip ( gpbxBattles , \"The four first battles in the field. A battle is two drivers less than 3 seconds apart\" ); tip1 . SetToolTip ( gpbxLapTimes , \"The fastest and slowest drivers on track at the moment. It takes the average lapTime of the last 5 laps to choose who is the fastes or the slowest\" ); RefreshUI (); } /// <summary> /// Will start or stop the process of decoding /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private async void btnUpdate_Click ( object sender , EventArgs e ) { cancelRequested = false ; if ( Emulator != null && Wrapper != null ) { // Disable UI controls to prevent re-entrancy btnResetEmulator . Enabled = false ; btnStartDecoding . Enabled = false ; btnStopUpdating . Enabled = true ; btnSettings . Enabled = false ; while (! cancelRequested ) { await semaphore . WaitAsync (); try { // Start the time-consuming task on a separate thread await Task . Run ( async () => { Stopwatch sw = new Stopwatch (); sw . Start (); Bitmap screen = Emulator . Screenshot (); screen . Save ( \"HopefullyDataScreenshot.png\" ); Invoke (( MethodInvoker ) delegate { pbxResult . Image = ( Bitmap ) screen . Clone (); }); Wrapper . ChangeImage ( screen ); int errorCode = Wrapper . Refresh (); sw . Stop (); // Task completed Invoke (( MethodInvoker ) delegate { DisplayResults ( errorCode , sw , screen ); DisplayBattles (); DisplayDeltas (); DisplayOvertakes (); }); }); } finally { semaphore . Release (); } } // Re-enable UI controls btnStopUpdating . Text = \"Stop\" ; btnStartDecoding . Enabled = true ; btnStopUpdating . Enabled = false ; btnResetEmulator . Enabled = true ; btnSettings . Enabled = true ; } } /// <summary> /// Will display the overtakes in the overtakes list box /// </summary> private void DisplayOvertakes () { Wrapper . DisplayOvertakes ( lsbOvertakes ); } /// <summary> /// Will display the battles in the battles pannel /// </summary> private void DisplayBattles () { Wrapper . DisplayBattles ( pnlBattles , this ); } /// <summary> /// Will display the time differences in the faster and slowest pannels /// </summary> private void DisplayDeltas () { Wrapper . DisplayTimesDeltas ( pnlFastest , pnlSlowest , this ); } /// <summary> /// Will try to stop the emulator (usually does not work please do not count on it) /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void Form1_FormClosing ( object sender , FormClosingEventArgs e ) { if ( Emulator != null ) { Emulator . Stop (); } } /// <summary> /// Will display the live ranking on the live ranking pannel. Its called like this because historically it was the method that just recovered the bare results from the OCR /// </summary> /// <param name=\"errorCode\"></param> /// <param name=\"sw\"></param> /// <param name=\"screen\"></param> private void DisplayResults ( int errorCode , Stopwatch sw , Bitmap screen ) { if ( errorCode != 0 ) { cancelRequested = true ; MessageBox . Show ( \"An error has occured while trying to recover data from live feed. This can happen sometimes. I would advise you to restart a few times. If the problem persists check your configuration.\" ); } else { Wrapper . DisplayLiveRanking ( pnlLiveRanking , this ); } } /// <summary> /// Will stop the data recovering operation and resets some buttons and text /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnStopUpdating_Click ( object sender , EventArgs e ) { // Set the cancellation flag cancelRequested = true ; btnStopUpdating . Enabled = false ; btnResetEmulator . Enabled = false ; btnStopUpdating . Text = \"Stopping\" ; } /// <summary> /// Will start the F1TVEmulator, again this name is historical because back at the start of this project this button did not have a name /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private async void button1_Click ( object sender , EventArgs e ) { lsbOvertakes . Items . Clear (); btnResetEmulator . Text = \"Launching\" ; btnResetEmulator . Enabled = false ; btnSettings . Enabled = true ; btnStartDecoding . Enabled = false ; btnStopUpdating . Enabled = false ; btnSettings . Enabled = false ; int errorCode = - 1 ; await Task . Run ( async () => { if ( Emulator != null ) Emulator . ResetDriver (); Emulator = null ; Wrapper = null ; GC . Collect (); Emulator = new F1TVEmulator ( GpUrl ); errorCode = await Emulator . Start (); }); if ( errorCode != 0 ) { string message = \"\" ; switch ( errorCode ) { case 100 : message = \"Error \" + errorCode + \" Could not recover cookies. It could be because of an improper installation of python or bad cookies in the chrome database. Please try to log on to the F1TV using chrome again\" ; break ; 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 ); btnResetEmulator . Enabled = true ; btnSettings . Enabled = true ; btnResetEmulator . Text = \"Retry\" ; } else { Wrapper = new DataWrapper ( ConfigFile , Emulator . Screenshot ()); btnResetEmulator . Text = \"Re launch\" ; btnResetEmulator . Enabled = true ; btnSettings . Enabled = true ; btnStartDecoding . Enabled = true ; } } /// <summary> /// Silly way to remove borders from groupbox and make them look like pannels with titles /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void removeBorders ( object sender , PaintEventArgs e ) { GroupBox gpbx = ( GroupBox ) sender ; using ( Pen pen = new Pen ( gpbx . BackColor , 50 )) { e . Graphics . DrawRectangle ( pen , 0 , 0 , gpbx . Width - 1 , gpbx . Height - 1 ); e . Graphics . DrawRectangle ( pen , 0 , 0 , gpbx . Width - 1 , gpbx . Height - 1 ); } using ( var brush = new SolidBrush ( gpbx . ForeColor )) { var textPosition = new Point ( 5 , 0 ); // Adjust the X and Y values as needed e . Graphics . DrawString ( gpbx . Text , gpbx . Font , brush , textPosition ); } } /// <summary> /// Will change the preset to use when starting the emulator /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void lsbPresets_SelectedIndexChanged ( object sender , EventArgs e ) { if ( lsbPresets . SelectedIndex >= 0 ) ConfigFile = lsbPresets . Items [ lsbPresets . SelectedIndex ]. ToString (); } /// <summary> /// Will change the URL the emulator will use, historical name again /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void textBox1_TextChanged ( object sender , EventArgs e ) { if ( tbxGpUrl . Text != \"\" ) GpUrl = tbxGpUrl . Text ; } /// <summary> /// This is called by the automatically generated buttons. Its here to fill in the driver info tab whenever the user clicks on a button that contains the name of a driver /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> public void btnDriver_Click ( object sender , EventArgs e ) { //Removes the cover if ( pnlCover . Visible = true ) pnlCover . Visible = false ; //Happens when a driver button has been clicked //MessageBox.Show((sender as Button).Name + \" has been selected\"); Button btn = ( sender as Button ); string [] parts = btn . Name . Split ( '_' ); DriverData driver = Wrapper . GetFullDriverData ( parts [ 0 ], pnlCurrentDriverLapsHistory , this ); lblCurrentDriverName . Text = driver . Name ; lblCurrentDriverPosition . Text = driver . Position . ToString (); lblCurrentDriverGapToLeader . Text = Reader . ConvertMsToTime ( driver . GapToLeader ); lblCurrentDriverLapTime . Text = Reader . ConvertMsToTime ( driver . LapTime ); lblCurrentDriverTyreAge . Text = driver . CurrentTyre . NumberOfLaps . ToString (); if ( driver . DRS ) { lblCurrentDriverDRS . Text = \"Open\" ; lblCurrentDriverDRS . ForeColor = Color . FromArgb ( 0 , 164 , 46 ); } else { lblCurrentDriverDRS . Text = \"Closed\" ; lblCurrentDriverDRS . ForeColor = Color . Black ; } switch ( driver . CurrentTyre . Coumpound ) { case Tyre . Type . Undefined : lblCurrentDriverTyreType . Text = \"uuuuh...\" ; lblCurrentDriverTyreType . ForeColor = Color . Violet ; break ; case Tyre . Type . Hard : lblCurrentDriverTyreType . Text = \"Hard\" ; lblCurrentDriverTyreType . ForeColor = Color . FromArgb ( 164 , 165 , 168 ); break ; case Tyre . Type . Medium : lblCurrentDriverTyreType . Text = \"Medium\" ; lblCurrentDriverTyreType . ForeColor = Color . FromArgb ( 245 , 191 , 0 ); break ; case Tyre . Type . Soft : lblCurrentDriverTyreType . Text = \"Soft\" ; lblCurrentDriverTyreType . ForeColor = Color . FromArgb ( 255 , 0 , 0 ); break ; case Tyre . Type . Inter : lblCurrentDriverTyreType . Text = \"Intermediate\" ; lblCurrentDriverTyreType . ForeColor = Color . FromArgb ( 0 , 164 , 46 ); break ; case Tyre . Type . Wet : lblCurrentDriverTyreType . Text = \"Wet\" ; lblCurrentDriverTyreType . ForeColor = Color . FromArgb ( 39 , 96 , 166 ); break ; } } /// <summary> /// This is supposed to be called by an automatically generated button. It should be any button with a laptime info on it /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> public void btnLapTime_Click ( object sender , EventArgs e ) { //Happens when a lapTime has been clicked Button btn = sender as Button ; string [] parts = btn . Name . Split ( '_' ); Wrapper . DisplayLapTimeInfos ( parts [ 0 ], Convert . ToInt32 ( parts [ 1 ]), btn . Text ); } /// <summary> /// Will trigger responsive calculation everytime the form changes size /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void Main_Resize ( object sender , EventArgs e ) { int xDiff = this . Width - oldSize . Width ; int yDiff = this . Height - oldSize . Height ; int padding = 10 ; //This will take half the newly created space gpbxRanking . Size = new Size ( oldRankingSize . Width + xDiff / 2 , oldRankingSize . Height + yDiff ); gpbxRanking . Location = new Point ( oldRankingPosition . X + xDiff / 2 , gpbxRanking . Location . Y ); //Will take half the new height and half the new height gpbxLapTimes . Size = new Size ( oldLapTimesSize . Width + xDiff / 2 , oldLapTimesSize . Height + yDiff / 2 ); //Will take half the new height and half the new width gpbxBattles . Size = new Size ( oldBattles . Width + xDiff / 2 , oldBattles . Height + yDiff / 2 ); gpbxBattles . Location = new Point ( gpbxBattles . Location . X , oldBattlePosition . Y + yDiff / 2 ); //The infos wont change width but will need to be centerd Point startOfZone = new Point ( gpbxOvertakes . Width + gpbxOvertakes . Location . X , gpbxOvertakes . Location . Y ); Point endOfZone = new Point ( gpbxRanking . Location . X , gpbxOvertakes . Location . Y ); int totalWidth = endOfZone . X - startOfZone . X ; gpbxDriverInfos . Location = new Point ( startOfZone . X + ( totalWidth / 2 - gpbxDriverInfos . Width / 2 ), oldDriverInfoPosition . Y + yDiff ); //Now resizing internals pnlFastest . Size = new Size ( oldPnlFastest . Width + xDiff / 4 , oldPnlFastest . Height + yDiff / 4 ); pnlFastest . Location = new Point ( olPnlFastestPosition . X , olPnlFastestPosition . Y + yDiff / 4 ); pnlSlowest . Size = new Size ( oldPnlSlowest . Width + xDiff / 4 , oldPnlSlowest . Height + yDiff / 4 ); pnlSlowest . Location = new Point ( oldPnlSlowestPosition . X + xDiff / 4 , oldPnlSlowestPosition . Y + yDiff / 4 ); pnlBattles . Size = new Size ( oldPnlBattles . Width + xDiff / 2 , oldPnlBattles . Height + yDiff / 2 ); pnlLiveRanking . Size = new Size ( oldPnlRankings . Width + xDiff / 2 , oldPnlRankings . Height + yDiff ); } } }","title":"Form1.cs"},{"location":"Code/SqliteStorage.html","text":"SqliteStorage.cs /// Author : Maxime Rohmer /// Date : 09/08/2023 /// File : SqliteStorage.cs /// Brief : Class that controls the sqlite database /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using System.Data.SQLite ; using System.IO ; using System.Windows.Forms ; namespace TrackTrends { public class SqliteStorage { private const string DATABASE_FOLDER = \"./Data\" ; private const string DATABASE_FILE = \"/database.sqlite\" ; private const string CONNECTION_STRING = \"Data Source=\" + DATABASE_FOLDER + DATABASE_FILE + \";Version=3;\" ; private SQLiteConnection Connection ; /// <summary> /// Creates a new Sqlite Storage and initialize the database /// </summary> public SqliteStorage () { Load (); } /// <summary> /// Loads a fresh new Database or create a new one if it does not exist. /// </summary> private void Load () { if (! Directory . Exists ( DATABASE_FOLDER )) Directory . CreateDirectory ( DATABASE_FOLDER ); if (! File . Exists ( DATABASE_FOLDER + DATABASE_FILE )) { SQLiteConnection . CreateFile ( DATABASE_FOLDER + DATABASE_FILE ); } else { //We are not using the existing DataBase File . Delete ( DATABASE_FOLDER + DATABASE_FILE ); } Connection = new SQLiteConnection ( CONNECTION_STRING ); Connection . Open (); //Create the drivers table string createDriversTableQuery = @\"CREATE TABLE IF NOT EXISTS Drivers (ID INTEGER PRIMARY KEY AUTOINCREMENT, Name VARCHAR NOT NULL);\" ; using ( var command = new SQLiteCommand ( createDriversTableQuery , Connection )) { command . ExecuteNonQuery (); } //Create the drivers table string createPitstopTableQuery = @\"CREATE TABLE Pitstops (Lap INTEGER NOT NULL, DriverID INTEGER NOT NULL, Tyre VARCHAR, PRIMARY KEY (Lap,DriverID));\" ; using ( var command = new SQLiteCommand ( createPitstopTableQuery , Connection )) { command . ExecuteNonQuery (); } //Create the stats string createStatsTableQuery = @\"CREATE TABLE IF NOT EXISTS Stats (Lap INTEGER NOT NULL, DriverID INTEGER NOT NULL, Tyre VARCHAR NOT NULL, LapTime INTEGER NOT NULL, Sector1 INTEGER NOT NULL, Sector2 INTEGER NOT NULL, Sector3 INTEGER NOT NULL, GapToLeader INTEGER NOT NULL, Position INTEGER NOT NULL, PRIMARY KEY (Lap, DriverID));\" ; using ( var command = new SQLiteCommand ( createStatsTableQuery , Connection )) { command . ExecuteNonQuery (); } } /// <summary> /// Adds a driver into the drivers table. Meant to be used at the start of the programm /// </summary> /// <param name=\"name\">The name of the driver. (non case sensitive)</param> public void AddDriver ( string name ) { string insertQuery = \"INSERT INTO Drivers (Name) VALUES (@name);\" ; using ( var command = new SQLiteCommand ( insertQuery , Connection )) { command . Parameters . AddWithValue ( \"@Name\" , name ); try { command . ExecuteNonQuery (); } catch { //MessageBox.Show(\"An error has occured while trying to insert a new driver into de Database\"); } } } /// <summary> /// Searches for a driver and returns its id if it has been found /// </summary> /// <param name=\"name\">Name of the driver (non case sensitive)</param> /// <returns></returns> private int GetDriverID ( string name ) { string selectQuery = \"SELECT ID FROM Drivers where Name LIKE @driverName\" ; int result = 0 ; using ( var command = new SQLiteCommand ( selectQuery , Connection )) { command . Parameters . AddWithValue ( \"@driverName\" , name ); try { using ( var reader = command . ExecuteReader ()) { while ( reader . Read ()) { result = reader . GetInt32 ( 0 ); } } } catch { //MessageBox.Show(\"There has been an error while trying to retrieve the ID of a Driver from the database\"); } } return result ; } /// <summary> /// Gets the sectors from a lapTime. Sectors are subdivisions of a laptime (could be usefull to validate one or the other) /// </summary> /// <param name=\"driverName\">The name of the driver who has done the lap</param> /// <param name=\"lap\">The lap at wich the driver has done the time</param> /// <returns>A list of the different sectors time in int (ms)</returns> public List < int > GetSectorsFromLapTime ( string driverName , int lap ) { int driverId = GetDriverID ( driverName ); string selectQuery = \"SELECT Sector1,Sector2,Sector3 FROM Stats WHERE DriverID = @driverID AND Lap = @lap\" ; List < int > result = new List < int >(); using ( var command = new SQLiteCommand ( selectQuery , Connection )) { command . Parameters . AddWithValue ( \"@driverID\" , driverId ); command . Parameters . AddWithValue ( \"@lap\" , lap ); try { SQLiteDataReader reader = command . ExecuteReader (); while ( reader . Read ()) { result . Add ( reader . GetInt32 ( 0 )); result . Add ( reader . GetInt32 ( 1 )); result . Add ( reader . GetInt32 ( 2 )); } } catch { //MessageBox.Show(\"There has been an error while trying to retrieve the ID of a Driver from the database\"); } } return result ; } /// <summary> /// Get the laptime history of a driver /// </summary> /// <param name=\"driverName\">The name of the driver</param> /// <param name=\"numberOfLaptimes\">The number of lapTimes you want</param> /// <returns>A list of tuples with the lap and the laptime. It will only return the amount it found so even if you ask 5 expect getting less or even 0</returns> public List <( int LapTime , int Lap )> GetDriverLaptimes ( string driverName , int numberOfLaptimes ) { int driverId = GetDriverID ( driverName ); List <( int LapTime , int Lap )> lapData = new List <( int LapTime , int Lap )>(); string selectQuery = \"Select LapTime,Lap from Stats WHERE DriverID = @driverID ORDER BY Lap DESC LIMIT @limit\" ; using ( var command = new SQLiteCommand ( selectQuery , Connection )) { command . Parameters . AddWithValue ( \"@driverID\" , driverId ); command . Parameters . AddWithValue ( \"@limit\" , numberOfLaptimes ); try { SQLiteDataReader reader = command . ExecuteReader (); while ( reader . Read ()) { int lapTime = reader . GetInt32 ( 0 ); int lap = reader . GetInt32 ( 1 ); lapData . Add (( lapTime , lap )); } } catch { //MessageBox.Show(\"There has been an error while trying to retrieve the ID of a Driver from the database\"); } } return lapData ; } /// <summary> /// Add a pitstop into the db /// </summary> /// <param name=\"driverName\">The name of the driver who made his pitstop</param> /// <param name=\"lap\">The lap where he stopped</param> /// <param name=\"tyre\">The tyre he took out</param> public void AddPitstop ( string driverName , int lap , string tyre ) { string insertQuery = \"INSERT INTO Pitstops (Lap,DriverID,Tyre) VALUES (@Lap,@DriverID,@Tyre)\" ; using ( var command = new SQLiteCommand ( insertQuery , Connection )) { command . Parameters . AddWithValue ( \"@Lap\" , lap ); command . Parameters . AddWithValue ( \"@DriverID\" , GetDriverID ( driverName )); command . Parameters . AddWithValue ( \"@Tyre\" , tyre ); try { command . ExecuteNonQuery (); } catch { //MessageBox.Show(\"An error has occured while trying to insert a new pitstop into the DB\" + Environment.NewLine + \"Request :\"+ command.ToString()); } } } /// <summary> /// Adds drivers info into the DB (it should only be once per lap) /// </summary> /// <param name=\"data\">The Driver data</param> /// <param name=\"lap\">The lap from wich the datas are from</param> public void AddDriverStat ( DriverData data , int lap ) { string insertQuery = \"INSERT INTO Stats (Lap,DriverID,Tyre,LapTime,Sector1,Sector2,Sector3,GapToLeader,Position) VALUES (@Lap,@DriverID,@Tyre,@LapTime,@Sector1,@Sector2,@Sector3,@GapToLeader,@Position);\" ; using ( var command = new SQLiteCommand ( insertQuery , Connection )) { command . Parameters . AddWithValue ( \"@Lap\" , lap ); command . Parameters . AddWithValue ( \"@DriverID\" , GetDriverID ( data . Name )); command . Parameters . AddWithValue ( \"@Tyre\" , data . CurrentTyre . Coumpound . ToString ()); command . Parameters . AddWithValue ( \"@LapTime\" , data . LapTime ); command . Parameters . AddWithValue ( \"@Sector1\" , data . Sector1 ); command . Parameters . AddWithValue ( \"@Sector2\" , data . Sector2 ); command . Parameters . AddWithValue ( \"@Sector3\" , data . Sector3 ); command . Parameters . AddWithValue ( \"@GapToLeader\" , data . GapToLeader ); command . Parameters . AddWithValue ( \"@Position\" , data . Position ); try { command . ExecuteNonQuery (); } catch { //MessageBox.Show(\"An error has occured while trying to insert infos about a driver\"); } } } } }","title":"SqliteStorage.cs"},{"location":"Code/SqliteStorage.html#sqlitestoragecs","text":"/// Author : Maxime Rohmer /// Date : 09/08/2023 /// File : SqliteStorage.cs /// Brief : Class that controls the sqlite database /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using System.Data.SQLite ; using System.IO ; using System.Windows.Forms ; namespace TrackTrends { public class SqliteStorage { private const string DATABASE_FOLDER = \"./Data\" ; private const string DATABASE_FILE = \"/database.sqlite\" ; private const string CONNECTION_STRING = \"Data Source=\" + DATABASE_FOLDER + DATABASE_FILE + \";Version=3;\" ; private SQLiteConnection Connection ; /// <summary> /// Creates a new Sqlite Storage and initialize the database /// </summary> public SqliteStorage () { Load (); } /// <summary> /// Loads a fresh new Database or create a new one if it does not exist. /// </summary> private void Load () { if (! Directory . Exists ( DATABASE_FOLDER )) Directory . CreateDirectory ( DATABASE_FOLDER ); if (! File . Exists ( DATABASE_FOLDER + DATABASE_FILE )) { SQLiteConnection . CreateFile ( DATABASE_FOLDER + DATABASE_FILE ); } else { //We are not using the existing DataBase File . Delete ( DATABASE_FOLDER + DATABASE_FILE ); } Connection = new SQLiteConnection ( CONNECTION_STRING ); Connection . Open (); //Create the drivers table string createDriversTableQuery = @\"CREATE TABLE IF NOT EXISTS Drivers (ID INTEGER PRIMARY KEY AUTOINCREMENT, Name VARCHAR NOT NULL);\" ; using ( var command = new SQLiteCommand ( createDriversTableQuery , Connection )) { command . ExecuteNonQuery (); } //Create the drivers table string createPitstopTableQuery = @\"CREATE TABLE Pitstops (Lap INTEGER NOT NULL, DriverID INTEGER NOT NULL, Tyre VARCHAR, PRIMARY KEY (Lap,DriverID));\" ; using ( var command = new SQLiteCommand ( createPitstopTableQuery , Connection )) { command . ExecuteNonQuery (); } //Create the stats string createStatsTableQuery = @\"CREATE TABLE IF NOT EXISTS Stats (Lap INTEGER NOT NULL, DriverID INTEGER NOT NULL, Tyre VARCHAR NOT NULL, LapTime INTEGER NOT NULL, Sector1 INTEGER NOT NULL, Sector2 INTEGER NOT NULL, Sector3 INTEGER NOT NULL, GapToLeader INTEGER NOT NULL, Position INTEGER NOT NULL, PRIMARY KEY (Lap, DriverID));\" ; using ( var command = new SQLiteCommand ( createStatsTableQuery , Connection )) { command . ExecuteNonQuery (); } } /// <summary> /// Adds a driver into the drivers table. Meant to be used at the start of the programm /// </summary> /// <param name=\"name\">The name of the driver. (non case sensitive)</param> public void AddDriver ( string name ) { string insertQuery = \"INSERT INTO Drivers (Name) VALUES (@name);\" ; using ( var command = new SQLiteCommand ( insertQuery , Connection )) { command . Parameters . AddWithValue ( \"@Name\" , name ); try { command . ExecuteNonQuery (); } catch { //MessageBox.Show(\"An error has occured while trying to insert a new driver into de Database\"); } } } /// <summary> /// Searches for a driver and returns its id if it has been found /// </summary> /// <param name=\"name\">Name of the driver (non case sensitive)</param> /// <returns></returns> private int GetDriverID ( string name ) { string selectQuery = \"SELECT ID FROM Drivers where Name LIKE @driverName\" ; int result = 0 ; using ( var command = new SQLiteCommand ( selectQuery , Connection )) { command . Parameters . AddWithValue ( \"@driverName\" , name ); try { using ( var reader = command . ExecuteReader ()) { while ( reader . Read ()) { result = reader . GetInt32 ( 0 ); } } } catch { //MessageBox.Show(\"There has been an error while trying to retrieve the ID of a Driver from the database\"); } } return result ; } /// <summary> /// Gets the sectors from a lapTime. Sectors are subdivisions of a laptime (could be usefull to validate one or the other) /// </summary> /// <param name=\"driverName\">The name of the driver who has done the lap</param> /// <param name=\"lap\">The lap at wich the driver has done the time</param> /// <returns>A list of the different sectors time in int (ms)</returns> public List < int > GetSectorsFromLapTime ( string driverName , int lap ) { int driverId = GetDriverID ( driverName ); string selectQuery = \"SELECT Sector1,Sector2,Sector3 FROM Stats WHERE DriverID = @driverID AND Lap = @lap\" ; List < int > result = new List < int >(); using ( var command = new SQLiteCommand ( selectQuery , Connection )) { command . Parameters . AddWithValue ( \"@driverID\" , driverId ); command . Parameters . AddWithValue ( \"@lap\" , lap ); try { SQLiteDataReader reader = command . ExecuteReader (); while ( reader . Read ()) { result . Add ( reader . GetInt32 ( 0 )); result . Add ( reader . GetInt32 ( 1 )); result . Add ( reader . GetInt32 ( 2 )); } } catch { //MessageBox.Show(\"There has been an error while trying to retrieve the ID of a Driver from the database\"); } } return result ; } /// <summary> /// Get the laptime history of a driver /// </summary> /// <param name=\"driverName\">The name of the driver</param> /// <param name=\"numberOfLaptimes\">The number of lapTimes you want</param> /// <returns>A list of tuples with the lap and the laptime. It will only return the amount it found so even if you ask 5 expect getting less or even 0</returns> public List <( int LapTime , int Lap )> GetDriverLaptimes ( string driverName , int numberOfLaptimes ) { int driverId = GetDriverID ( driverName ); List <( int LapTime , int Lap )> lapData = new List <( int LapTime , int Lap )>(); string selectQuery = \"Select LapTime,Lap from Stats WHERE DriverID = @driverID ORDER BY Lap DESC LIMIT @limit\" ; using ( var command = new SQLiteCommand ( selectQuery , Connection )) { command . Parameters . AddWithValue ( \"@driverID\" , driverId ); command . Parameters . AddWithValue ( \"@limit\" , numberOfLaptimes ); try { SQLiteDataReader reader = command . ExecuteReader (); while ( reader . Read ()) { int lapTime = reader . GetInt32 ( 0 ); int lap = reader . GetInt32 ( 1 ); lapData . Add (( lapTime , lap )); } } catch { //MessageBox.Show(\"There has been an error while trying to retrieve the ID of a Driver from the database\"); } } return lapData ; } /// <summary> /// Add a pitstop into the db /// </summary> /// <param name=\"driverName\">The name of the driver who made his pitstop</param> /// <param name=\"lap\">The lap where he stopped</param> /// <param name=\"tyre\">The tyre he took out</param> public void AddPitstop ( string driverName , int lap , string tyre ) { string insertQuery = \"INSERT INTO Pitstops (Lap,DriverID,Tyre) VALUES (@Lap,@DriverID,@Tyre)\" ; using ( var command = new SQLiteCommand ( insertQuery , Connection )) { command . Parameters . AddWithValue ( \"@Lap\" , lap ); command . Parameters . AddWithValue ( \"@DriverID\" , GetDriverID ( driverName )); command . Parameters . AddWithValue ( \"@Tyre\" , tyre ); try { command . ExecuteNonQuery (); } catch { //MessageBox.Show(\"An error has occured while trying to insert a new pitstop into the DB\" + Environment.NewLine + \"Request :\"+ command.ToString()); } } } /// <summary> /// Adds drivers info into the DB (it should only be once per lap) /// </summary> /// <param name=\"data\">The Driver data</param> /// <param name=\"lap\">The lap from wich the datas are from</param> public void AddDriverStat ( DriverData data , int lap ) { string insertQuery = \"INSERT INTO Stats (Lap,DriverID,Tyre,LapTime,Sector1,Sector2,Sector3,GapToLeader,Position) VALUES (@Lap,@DriverID,@Tyre,@LapTime,@Sector1,@Sector2,@Sector3,@GapToLeader,@Position);\" ; using ( var command = new SQLiteCommand ( insertQuery , Connection )) { command . Parameters . AddWithValue ( \"@Lap\" , lap ); command . Parameters . AddWithValue ( \"@DriverID\" , GetDriverID ( data . Name )); command . Parameters . AddWithValue ( \"@Tyre\" , data . CurrentTyre . Coumpound . ToString ()); command . Parameters . AddWithValue ( \"@LapTime\" , data . LapTime ); command . Parameters . AddWithValue ( \"@Sector1\" , data . Sector1 ); command . Parameters . AddWithValue ( \"@Sector2\" , data . Sector2 ); command . Parameters . AddWithValue ( \"@Sector3\" , data . Sector3 ); command . Parameters . AddWithValue ( \"@GapToLeader\" , data . GapToLeader ); command . Parameters . AddWithValue ( \"@Position\" , data . Position ); try { command . ExecuteNonQuery (); } catch { //MessageBox.Show(\"An error has occured while trying to insert infos about a driver\"); } } } } }","title":"SqliteStorage.cs"},{"location":"Code/Zone.html","text":"Zone.cs /// Author : Maxime Rohmer /// Date : 09/06/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 : Beta 1.0 using System ; using System.Collections.Generic ; using System.Drawing ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; namespace TrackTrends { 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 = value ; 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 ; } /// <summary> /// Creates a new Zone /// </summary> /// <param name=\"image\">Image of the parent zone</param> /// <param name=\"bounds\">The position and size of the zone</param> /// <param name=\"name\">THe name of the zone (usefull for the JSON formatting)</param> 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 DriverData Decode ( List < string > driverList ) { int sectorCount = 0 ; DriverData result = new DriverData (); foreach ( Window w in Windows ) { // A switch would be prettier but I dont think its supported in this C# version if ( w is DriverNameWindow ) result . Name = ( string )( w as DriverNameWindow ). DecodePng ( driverList ); if ( w is DriverDrsWindow ) result . DRS = ( bool )( w as DriverDrsWindow ). DecodePng (); if ( w is DriverGapToLeaderWindow ) result . GapToLeader = ( int )( w as DriverGapToLeaderWindow ). DecodePng (); if ( w is DriverLapTimeWindow ) result . LapTime = ( int )( w as DriverLapTimeWindow ). DecodePng (); if ( w is DriverPositionWindow ) result . Position = ( int )( w as DriverPositionWindow ). DecodePng (); if ( w is DriverSectorWindow ) { sectorCount ++; if ( sectorCount == 1 ) result . Sector1 = ( int )( w as DriverSectorWindow ). DecodePng (); if ( sectorCount == 2 ) result . Sector2 = ( int )( w as DriverSectorWindow ). DecodePng (); if ( sectorCount == 3 ) result . Sector3 = ( int )( w as DriverSectorWindow ). DecodePng (); } if ( w is DriverTyresWindow ) result . CurrentTyre = ( Tyre )( 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 ( new SolidBrush ( Color . FromArgb ( 249 , 194 , 46 )), 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 ( new Pen ( new SolidBrush ( Color . FromArgb ( 249 , 194 , 46 )), 5 ), newBounds ); } foreach ( Window w in Windows ) { g . DrawRectangle ( new Pen ( new SolidBrush ( Color . FromArgb ( 252 , 252 , 252 )), 5 ), w . Bounds ); } return img ; } public void ResetZones () { Zones . Clear (); } public void ResetWindows () { foreach ( Zone z in Zones ) { z . ResetWindows (); } Windows . Clear (); } /// <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":"/// Author : Maxime Rohmer /// Date : 09/06/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 : Beta 1.0 using System ; using System.Collections.Generic ; using System.Drawing ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; namespace TrackTrends { 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 = value ; 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 ; } /// <summary> /// Creates a new Zone /// </summary> /// <param name=\"image\">Image of the parent zone</param> /// <param name=\"bounds\">The position and size of the zone</param> /// <param name=\"name\">THe name of the zone (usefull for the JSON formatting)</param> 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 DriverData Decode ( List < string > driverList ) { int sectorCount = 0 ; DriverData result = new DriverData (); foreach ( Window w in Windows ) { // A switch would be prettier but I dont think its supported in this C# version if ( w is DriverNameWindow ) result . Name = ( string )( w as DriverNameWindow ). DecodePng ( driverList ); if ( w is DriverDrsWindow ) result . DRS = ( bool )( w as DriverDrsWindow ). DecodePng (); if ( w is DriverGapToLeaderWindow ) result . GapToLeader = ( int )( w as DriverGapToLeaderWindow ). DecodePng (); if ( w is DriverLapTimeWindow ) result . LapTime = ( int )( w as DriverLapTimeWindow ). DecodePng (); if ( w is DriverPositionWindow ) result . Position = ( int )( w as DriverPositionWindow ). DecodePng (); if ( w is DriverSectorWindow ) { sectorCount ++; if ( sectorCount == 1 ) result . Sector1 = ( int )( w as DriverSectorWindow ). DecodePng (); if ( sectorCount == 2 ) result . Sector2 = ( int )( w as DriverSectorWindow ). DecodePng (); if ( sectorCount == 3 ) result . Sector3 = ( int )( w as DriverSectorWindow ). DecodePng (); } if ( w is DriverTyresWindow ) result . CurrentTyre = ( Tyre )( 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 ( new SolidBrush ( Color . FromArgb ( 249 , 194 , 46 )), 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 ( new Pen ( new SolidBrush ( Color . FromArgb ( 249 , 194 , 46 )), 5 ), newBounds ); } foreach ( Window w in Windows ) { g . DrawRectangle ( new Pen ( new SolidBrush ( Color . FromArgb ( 252 , 252 , 252 )), 5 ), w . Bounds ); } return img ; } public void ResetZones () { Zones . Clear (); } public void ResetWindows () { foreach ( Zone z in Zones ) { z . ResetWindows (); } Windows . Clear (); } /// <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/DataWrapper.html","text":"DataWrapper.cs /// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DataWrapper.cs /// Brief : Class that is used to interface between the main Form (vue) and the Storage (wich is a class that wraps the sqlite database, so the model) its almost MVC :D /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using System.Windows.Forms ; using System.Drawing ; namespace TrackTrends { internal class DataWrapper { private Reader Reader ; private SqliteStorage Storage ; List < List < DriverData >> LiveDriverDataLogs = new List < List < DriverData >>(); //Note : It could be usefull to get the mainForm at the start of the programm and not have to take it in half of the methods. /// <summary> /// Constructs a new DataWrapper. It needs the config file so it can create a Reader, It also needs a first screenshot for the same reason /// </summary> /// <param name=\"configFile\">The JSON config file that is created by the configuration tool</param> /// <param name=\"screenshot\">A screenshot of the </param> public DataWrapper ( string configFile , Bitmap screenshot ) { Reader = new Reader ( configFile , screenshot , true ); //The Storage is here and on the Reader. It seems bad but it is ok as we dont use it at all to insert data and are only using it here to read some. The reader takes care of the inserts (Note: We could technically do both here but I did not find it usefull to transfer everything here) Storage = Reader . Storage ; } /// <summary> /// Refreshes the controller so it has the latest driver datas (Be sure to call it everytime you need to use any other method and expects the data to be up to date) /// </summary> /// <returns>Error code, 0 is success, 1 is not (Note: Maybe it could be interesting in the future to add some more error handling here)</returns> public int Refresh () { LiveDriverDataLogs . Add ( Reader . Decode ( Reader . MainZones , Reader . Drivers )); if ( LiveDriverDataLogs . Count > 0 ) return 0 ; return 1 ; } /// <summary> /// Changes the image to the newest screenshot in all of the zones and windows /// </summary> /// <param name=\"image\">The new screenshot to put everywhere (Do not mix resolutions)</param> public void ChangeImage ( Bitmap image ) { Reader . ChangeImage ( image ); } /// <summary> /// Gets all the data from one driver and also displays into the given panel the last five laps (or less if its the sart of the race) Note: Its responsive :D /// </summary> /// <param name=\"driverName\">The name of the driver (should not be case sensitive but it MUST already exist in the first list that has been inserted into the DB)</param> /// <param name=\"lastFiveLapsPanel\">The pannel where you want the five last laps to be displayed</param> /// <param name=\"form1\">The Main form.</param> /// <returns></returns> public DriverData GetFullDriverData ( string driverName , Panel lastFiveLapsPanel , Main form1 ) { //Note : I know that its a bad idea to ask the Form in this method and some others because it means that it wont work with any main form. And to that Ill say that... your right ! DriverData result = new DriverData (); if ( LiveDriverDataLogs . Count > 0 ) { //Searches the most recent live data from the given driverName foreach ( DriverData data in LiveDriverDataLogs [ LiveDriverDataLogs . Count - 1 ]) { if ( data . Name == driverName ) result = data ; } if ( result . Name != \"\" ) { //Recovers and displays the last five laps from the driver lastFiveLapsPanel . Controls . Clear (); Size labelDimensions = new Size ( lastFiveLapsPanel . Width , lastFiveLapsPanel . Height / 5 ); List <( int LapTime , int Lap )> lapsInfos = Storage . GetDriverLaptimes ( driverName , 5 ); int id = 0 ; foreach (( int LapTime , int Lap ) lapData in lapsInfos ) { //Hardcodes the new button. //Note : It could be smart to have like a default button for all the methods to use without needing to rewrite everything. Button newButton = new Button (); lastFiveLapsPanel . Controls . Add ( newButton ); newButton . Name = driverName + \"_\" + lapData . Lap ; newButton . Text = Reader . ConvertMsToTime ( lapData . LapTime ); newButton . Size = labelDimensions ; newButton . FlatStyle = FlatStyle . Popup ; newButton . Click += form1 . btnLapTime_Click ; newButton . Location = new Point ( 0 , id * newButton . Height ); id ++; } } } return result ; } /// <summary> /// Runs trough every drivers live data to recover the drivers that are close to each others /// </summary> /// <param name=\"pnlBattles\">The control that will host the displayed battles</param> /// <param name=\"form1\">The main form. It needs to have a method called 'btnDriver_Click' so it can reads the buttons clicks</param> public void DisplayBattles ( Panel pnlBattles , Main form1 ) { DriverData oldDriver = null ; List <( DriverData d1 , DriverData d2 , int gap )> battles = new List <( DriverData d1 , DriverData d2 , int gap )>(); //Search trough all the drivers and finds the one battling foreach ( DriverData driver in LiveDriverDataLogs [ LiveDriverDataLogs . Count - 1 ]) { if ( oldDriver != null && driver . Position != - 1 && oldDriver . Position != - 1 ) { if ( driver . GapToLeader < oldDriver . GapToLeader ) { //There is a problem with the drivers gaps } else { int gap = driver . GapToLeader - oldDriver . GapToLeader ; //3000ms is 3s. If drivers are that close then they are definitely in battle. If they are farther then maybe not if ( gap <= 3000 ) { battles . Add (( oldDriver , driver , gap )); } } oldDriver = driver ; } else { oldDriver = driver ; } } //We will only display 4 battles max int maxBattles = 4 ; if ( battles . Count > 0 ) { pnlBattles . Controls . Clear (); int maxUiHeight = Math . Max ( pnlBattles . Height / maxBattles , pnlBattles . Height / battles . Count ); int id = 0 ; foreach (( DriverData d1 , DriverData d2 , int gap ) battle in battles ) { if ( id < maxBattles ) { //*hardcoding* the different controls that needs to be added to the panel. //Note : this stuff could totally be handled by the Form with method returning a list of the drivers. It was just easier for me at the time to code it this way but its not the prettiest Button btnFirstDriver = new Button (); Button btnSecondDriver = new Button (); Label lblGap = new Label (); pnlBattles . Controls . Add ( btnFirstDriver ); pnlBattles . Controls . Add ( lblGap ); pnlBattles . Controls . Add ( btnSecondDriver ); btnFirstDriver . Anchor = AnchorStyles . Left | AnchorStyles . Top ; btnSecondDriver . Anchor = AnchorStyles . Right | AnchorStyles . Top ; lblGap . Anchor = AnchorStyles . Right | AnchorStyles . Left | AnchorStyles . Top ; lblGap . TextAlign = ContentAlignment . MiddleCenter ; lblGap . Font = new Font ( lblGap . Font . FontFamily , 15 ); btnFirstDriver . Click += form1 . btnDriver_Click ; btnSecondDriver . Click += form1 . btnDriver_Click ; btnFirstDriver . FlatStyle = FlatStyle . Popup ; btnSecondDriver . FlatStyle = FlatStyle . Popup ; lblGap . FlatStyle = FlatStyle . Popup ; btnFirstDriver . Size = new Size ( pnlBattles . Width / 3 , maxUiHeight ); btnSecondDriver . Size = new Size ( pnlBattles . Width / 3 , maxUiHeight ); lblGap . Size = new Size ( pnlBattles . Width / 3 , maxUiHeight ); btnFirstDriver . Location = new Point ( pnlBattles . Width / 3 * 0 , id * maxUiHeight ); lblGap . Location = new Point ( pnlBattles . Width / 3 * 1 , id * maxUiHeight ); btnSecondDriver . Location = new Point ( pnlBattles . Width / 3 * 2 , id * maxUiHeight ); btnFirstDriver . Text = battle . d1 . Name ; lblGap . Text = \"+ \" + Reader . ConvertMsToTime ( battle . gap ); if ( battle . gap <= 2000 ) lblGap . ForeColor = Color . Yellow ; if ( battle . gap <= 1000 ) lblGap . ForeColor = Color . Green ; btnSecondDriver . Text = battle . d2 . Name ; btnFirstDriver . Name = battle . d1 . Name + \"_\" + id ; lblGap . Name = \"lbl_Gap_\" + id ; btnSecondDriver . Name = battle . d2 . Name + \"_\" + id ; } else { break ; } id ++; } } } /// <summary> /// Searches the fastest and slowests drivers and displays them in the given panels /// </summary> /// <param name=\"pnlFastest\">Panel that will contain the constructed controls</param> /// <param name=\"pnlSlowest\">Panel that will contain the constructed controls</param> /// <param name=\"form1\">The main form that needs to implement the method btnDriver_Click to allow it to recover custom buttons click</param> public void DisplayTimesDeltas ( Panel pnlFastest , Panel pnlSlowest , Main form1 ) { List <( int avg , string driverName )> averages = new List <( int avg , string driverName )>(); foreach ( DriverData driver in LiveDriverDataLogs [ LiveDriverDataLogs . Count - 1 ]) { //We want to recover the last 5 lap times List <( int lapTime , int lap )> laps = Storage . GetDriverLaptimes ( driver . Name , 5 ); if ( laps . Count > 0 ) { int avg = 0 ; foreach (( int lapTime , int lap ) lap in laps ) { avg += lap . lapTime ; } avg = avg / laps . Count ; averages . Add (( avg , driver . Name )); } } int numberOfDriversToShow = 5 ; if ( averages . Count > 0 && averages . Count > numberOfDriversToShow ) { averages = averages . OrderBy ( item => item . avg ). ToList (); pnlFastest . Controls . Clear (); pnlSlowest . Controls . Clear (); int maxUiSize = pnlFastest . Height / numberOfDriversToShow ; //Displays the fastest drivers for ( int i = 0 ; i < numberOfDriversToShow ; i ++) { Button newButton = new Button (); ( int avg , string driver ) data = averages [ i ]; pnlFastest . Controls . Add ( newButton ); newButton . Size = new Size ( pnlFastest . Width , maxUiSize ); newButton . Location = new Point ( 0 , i * maxUiSize ); newButton . Text = data . driver ; newButton . FlatStyle = FlatStyle . Popup ; newButton . Name = data . driver + \"_fastest_\" + i ; newButton . Click += form1 . btnDriver_Click ; //We take the average time lost per lap if ( i != 0 ) newButton . Text += \" + \" + Reader . ConvertMsToTime ( Convert . ToInt32 ((( float ) data . avg - ( float ) averages [ 0 ]. avg ) / 5.0f )); } //Displays the slowests drivers int badId = 0 ; for ( int i = averages . Count - 1 ; i >= averages . Count - numberOfDriversToShow ; i --) { Button newButton = new Button (); ( int avg , string driver ) data = averages [ i ]; pnlSlowest . Controls . Add ( newButton ); newButton . Size = new Size ( pnlFastest . Width , maxUiSize ); newButton . Location = new Point ( 0 , badId * maxUiSize ); newButton . Text = data . driver ; newButton . FlatStyle = FlatStyle . Popup ; newButton . Name = data . driver + \"_slowest_\" + i ; newButton . Click += form1 . btnDriver_Click ; //We take the average time lost per lap newButton . Text += \" + \" + Reader . ConvertMsToTime ( Convert . ToInt32 ((( float ) data . avg - ( float ) averages [ 0 ]. avg ) / 5.0f )); badId ++; } } } /// <summary> /// Will add to the list of overtakes the different changes of position /// </summary> /// <param name=\"lsbResult\">The listbox containing all the infos</param> public void DisplayOvertakes ( ListBox lsbResult ) { //Note : This method SHOULD REALLY not do this but just return a string or a list of string with the new overtakes so the form can handle it as it wishes if ( LiveDriverDataLogs . Count > 1 ) { List < DriverData > oldList = LiveDriverDataLogs [ LiveDriverDataLogs . Count - 2 ]; List < DriverData > newList = LiveDriverDataLogs [ LiveDriverDataLogs . Count - 1 ]; for ( int i = 0 ; i < LiveDriverDataLogs [ LiveDriverDataLogs . Count - 1 ]. Count ; i ++) { if ( oldList [ i ]. Name != newList [ i ]. Name ) { //There has been a change in the standings for ( int y = 0 ; y < oldList . Count ; y ++) { if ( newList [ y ]. Name == oldList [ i ]. Name ) { //We found its new location if ( y > i ) { //The driver overtook someone lsbResult . Items . Add ( newList [ y ]. Name + \" climbed to \" + y ); } else { //The driver got overtook by someone lsbResult . Items . Add ( newList [ y ]. Name + \" fell to \" + y ); } } } } } } } /// <summary> /// Displays a messageBox containing the infos about a lap time /// </summary> /// <param name=\"driverName\">The name of the driver that has done the lapTime</param> /// <param name=\"Lap\">The number of the lap on wich the lapTime has been set (CAUTION ITS NOT THE RACING LAP ITS FROM THE DB)</param> /// <param name=\"LapTime\">The time (in ms) of the lap</param> public void DisplayLapTimeInfos ( string driverName , int Lap , string LapTime ) { List < int > sectors = Storage . GetSectorsFromLapTime ( driverName , Lap ); string message = \"Lap time infos\" + Environment . NewLine ; message += LapTime + Environment . NewLine ; if ( sectors . Count > 0 ) message += \"Sector 1 : \" + Reader . ConvertMsToTime ( sectors [ 0 ]) + Environment . NewLine ; if ( sectors . Count > 1 ) message += \"Sector 2 : \" + Reader . ConvertMsToTime ( sectors [ 1 ]) + Environment . NewLine ; if ( sectors . Count > 2 ) message += \"Sector 3 : \" + Reader . ConvertMsToTime ( sectors [ 2 ]) + Environment . NewLine ; MessageBox . Show ( message ); } /// <summary> /// Displays the live ranking with the names of the drivers and their gap to the leader in the right order /// </summary> /// <param name=\"pnl\">The control that will host all the new controls</param> /// <param name=\"form1\">The main form</param> public void DisplayLiveRanking ( Panel pnl , Main form1 ) { if ( LiveDriverDataLogs . Count > 0 ) { pnl . Controls . Clear (); //Gets the last item that should be the most recent data List < DriverData > liveData = LiveDriverDataLogs [ LiveDriverDataLogs . Count - 1 ]; Button [] buttons = new Button [ liveData . Count ]; Size buttonDimensions = new Size ( pnl . Width , pnl . Height / liveData . Count ); for ( int driverCount = 0 ; driverCount < liveData . Count ; driverCount ++) { Button newButton = new Button (); newButton . Size = buttonDimensions ; newButton . Location = new Point ( 0 , driverCount * buttonDimensions . Height ); newButton . FlatStyle = FlatStyle . Popup ; DriverData driver = liveData [ driverCount ]; if ( driver . Position == - 1 ) { //Its a DNF newButton . Enabled = false ; } if ( driver . Position > 1 ) { newButton . Text = driver . Name + \" +\" + Reader . ConvertMsToTime ( driver . GapToLeader ); } else { newButton . Text = driver . Name ; } newButton . Name = liveData [ driverCount ]. Name ; newButton . TextAlign = ContentAlignment . MiddleLeft ; newButton . FlatStyle = FlatStyle . Popup ; newButton . Click += form1 . btnDriver_Click ; buttons [ driverCount ] = newButton ; } //Note : It could be better to have this directly in the same loop foreach ( Button button in buttons ) { pnl . Controls . Add ( button ); } } } } }","title":"DataWrapper.cs"},{"location":"Code/DataWrapper.html#datawrappercs","text":"/// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DataWrapper.cs /// Brief : Class that is used to interface between the main Form (vue) and the Storage (wich is a class that wraps the sqlite database, so the model) its almost MVC :D /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using System.Windows.Forms ; using System.Drawing ; namespace TrackTrends { internal class DataWrapper { private Reader Reader ; private SqliteStorage Storage ; List < List < DriverData >> LiveDriverDataLogs = new List < List < DriverData >>(); //Note : It could be usefull to get the mainForm at the start of the programm and not have to take it in half of the methods. /// <summary> /// Constructs a new DataWrapper. It needs the config file so it can create a Reader, It also needs a first screenshot for the same reason /// </summary> /// <param name=\"configFile\">The JSON config file that is created by the configuration tool</param> /// <param name=\"screenshot\">A screenshot of the </param> public DataWrapper ( string configFile , Bitmap screenshot ) { Reader = new Reader ( configFile , screenshot , true ); //The Storage is here and on the Reader. It seems bad but it is ok as we dont use it at all to insert data and are only using it here to read some. The reader takes care of the inserts (Note: We could technically do both here but I did not find it usefull to transfer everything here) Storage = Reader . Storage ; } /// <summary> /// Refreshes the controller so it has the latest driver datas (Be sure to call it everytime you need to use any other method and expects the data to be up to date) /// </summary> /// <returns>Error code, 0 is success, 1 is not (Note: Maybe it could be interesting in the future to add some more error handling here)</returns> public int Refresh () { LiveDriverDataLogs . Add ( Reader . Decode ( Reader . MainZones , Reader . Drivers )); if ( LiveDriverDataLogs . Count > 0 ) return 0 ; return 1 ; } /// <summary> /// Changes the image to the newest screenshot in all of the zones and windows /// </summary> /// <param name=\"image\">The new screenshot to put everywhere (Do not mix resolutions)</param> public void ChangeImage ( Bitmap image ) { Reader . ChangeImage ( image ); } /// <summary> /// Gets all the data from one driver and also displays into the given panel the last five laps (or less if its the sart of the race) Note: Its responsive :D /// </summary> /// <param name=\"driverName\">The name of the driver (should not be case sensitive but it MUST already exist in the first list that has been inserted into the DB)</param> /// <param name=\"lastFiveLapsPanel\">The pannel where you want the five last laps to be displayed</param> /// <param name=\"form1\">The Main form.</param> /// <returns></returns> public DriverData GetFullDriverData ( string driverName , Panel lastFiveLapsPanel , Main form1 ) { //Note : I know that its a bad idea to ask the Form in this method and some others because it means that it wont work with any main form. And to that Ill say that... your right ! DriverData result = new DriverData (); if ( LiveDriverDataLogs . Count > 0 ) { //Searches the most recent live data from the given driverName foreach ( DriverData data in LiveDriverDataLogs [ LiveDriverDataLogs . Count - 1 ]) { if ( data . Name == driverName ) result = data ; } if ( result . Name != \"\" ) { //Recovers and displays the last five laps from the driver lastFiveLapsPanel . Controls . Clear (); Size labelDimensions = new Size ( lastFiveLapsPanel . Width , lastFiveLapsPanel . Height / 5 ); List <( int LapTime , int Lap )> lapsInfos = Storage . GetDriverLaptimes ( driverName , 5 ); int id = 0 ; foreach (( int LapTime , int Lap ) lapData in lapsInfos ) { //Hardcodes the new button. //Note : It could be smart to have like a default button for all the methods to use without needing to rewrite everything. Button newButton = new Button (); lastFiveLapsPanel . Controls . Add ( newButton ); newButton . Name = driverName + \"_\" + lapData . Lap ; newButton . Text = Reader . ConvertMsToTime ( lapData . LapTime ); newButton . Size = labelDimensions ; newButton . FlatStyle = FlatStyle . Popup ; newButton . Click += form1 . btnLapTime_Click ; newButton . Location = new Point ( 0 , id * newButton . Height ); id ++; } } } return result ; } /// <summary> /// Runs trough every drivers live data to recover the drivers that are close to each others /// </summary> /// <param name=\"pnlBattles\">The control that will host the displayed battles</param> /// <param name=\"form1\">The main form. It needs to have a method called 'btnDriver_Click' so it can reads the buttons clicks</param> public void DisplayBattles ( Panel pnlBattles , Main form1 ) { DriverData oldDriver = null ; List <( DriverData d1 , DriverData d2 , int gap )> battles = new List <( DriverData d1 , DriverData d2 , int gap )>(); //Search trough all the drivers and finds the one battling foreach ( DriverData driver in LiveDriverDataLogs [ LiveDriverDataLogs . Count - 1 ]) { if ( oldDriver != null && driver . Position != - 1 && oldDriver . Position != - 1 ) { if ( driver . GapToLeader < oldDriver . GapToLeader ) { //There is a problem with the drivers gaps } else { int gap = driver . GapToLeader - oldDriver . GapToLeader ; //3000ms is 3s. If drivers are that close then they are definitely in battle. If they are farther then maybe not if ( gap <= 3000 ) { battles . Add (( oldDriver , driver , gap )); } } oldDriver = driver ; } else { oldDriver = driver ; } } //We will only display 4 battles max int maxBattles = 4 ; if ( battles . Count > 0 ) { pnlBattles . Controls . Clear (); int maxUiHeight = Math . Max ( pnlBattles . Height / maxBattles , pnlBattles . Height / battles . Count ); int id = 0 ; foreach (( DriverData d1 , DriverData d2 , int gap ) battle in battles ) { if ( id < maxBattles ) { //*hardcoding* the different controls that needs to be added to the panel. //Note : this stuff could totally be handled by the Form with method returning a list of the drivers. It was just easier for me at the time to code it this way but its not the prettiest Button btnFirstDriver = new Button (); Button btnSecondDriver = new Button (); Label lblGap = new Label (); pnlBattles . Controls . Add ( btnFirstDriver ); pnlBattles . Controls . Add ( lblGap ); pnlBattles . Controls . Add ( btnSecondDriver ); btnFirstDriver . Anchor = AnchorStyles . Left | AnchorStyles . Top ; btnSecondDriver . Anchor = AnchorStyles . Right | AnchorStyles . Top ; lblGap . Anchor = AnchorStyles . Right | AnchorStyles . Left | AnchorStyles . Top ; lblGap . TextAlign = ContentAlignment . MiddleCenter ; lblGap . Font = new Font ( lblGap . Font . FontFamily , 15 ); btnFirstDriver . Click += form1 . btnDriver_Click ; btnSecondDriver . Click += form1 . btnDriver_Click ; btnFirstDriver . FlatStyle = FlatStyle . Popup ; btnSecondDriver . FlatStyle = FlatStyle . Popup ; lblGap . FlatStyle = FlatStyle . Popup ; btnFirstDriver . Size = new Size ( pnlBattles . Width / 3 , maxUiHeight ); btnSecondDriver . Size = new Size ( pnlBattles . Width / 3 , maxUiHeight ); lblGap . Size = new Size ( pnlBattles . Width / 3 , maxUiHeight ); btnFirstDriver . Location = new Point ( pnlBattles . Width / 3 * 0 , id * maxUiHeight ); lblGap . Location = new Point ( pnlBattles . Width / 3 * 1 , id * maxUiHeight ); btnSecondDriver . Location = new Point ( pnlBattles . Width / 3 * 2 , id * maxUiHeight ); btnFirstDriver . Text = battle . d1 . Name ; lblGap . Text = \"+ \" + Reader . ConvertMsToTime ( battle . gap ); if ( battle . gap <= 2000 ) lblGap . ForeColor = Color . Yellow ; if ( battle . gap <= 1000 ) lblGap . ForeColor = Color . Green ; btnSecondDriver . Text = battle . d2 . Name ; btnFirstDriver . Name = battle . d1 . Name + \"_\" + id ; lblGap . Name = \"lbl_Gap_\" + id ; btnSecondDriver . Name = battle . d2 . Name + \"_\" + id ; } else { break ; } id ++; } } } /// <summary> /// Searches the fastest and slowests drivers and displays them in the given panels /// </summary> /// <param name=\"pnlFastest\">Panel that will contain the constructed controls</param> /// <param name=\"pnlSlowest\">Panel that will contain the constructed controls</param> /// <param name=\"form1\">The main form that needs to implement the method btnDriver_Click to allow it to recover custom buttons click</param> public void DisplayTimesDeltas ( Panel pnlFastest , Panel pnlSlowest , Main form1 ) { List <( int avg , string driverName )> averages = new List <( int avg , string driverName )>(); foreach ( DriverData driver in LiveDriverDataLogs [ LiveDriverDataLogs . Count - 1 ]) { //We want to recover the last 5 lap times List <( int lapTime , int lap )> laps = Storage . GetDriverLaptimes ( driver . Name , 5 ); if ( laps . Count > 0 ) { int avg = 0 ; foreach (( int lapTime , int lap ) lap in laps ) { avg += lap . lapTime ; } avg = avg / laps . Count ; averages . Add (( avg , driver . Name )); } } int numberOfDriversToShow = 5 ; if ( averages . Count > 0 && averages . Count > numberOfDriversToShow ) { averages = averages . OrderBy ( item => item . avg ). ToList (); pnlFastest . Controls . Clear (); pnlSlowest . Controls . Clear (); int maxUiSize = pnlFastest . Height / numberOfDriversToShow ; //Displays the fastest drivers for ( int i = 0 ; i < numberOfDriversToShow ; i ++) { Button newButton = new Button (); ( int avg , string driver ) data = averages [ i ]; pnlFastest . Controls . Add ( newButton ); newButton . Size = new Size ( pnlFastest . Width , maxUiSize ); newButton . Location = new Point ( 0 , i * maxUiSize ); newButton . Text = data . driver ; newButton . FlatStyle = FlatStyle . Popup ; newButton . Name = data . driver + \"_fastest_\" + i ; newButton . Click += form1 . btnDriver_Click ; //We take the average time lost per lap if ( i != 0 ) newButton . Text += \" + \" + Reader . ConvertMsToTime ( Convert . ToInt32 ((( float ) data . avg - ( float ) averages [ 0 ]. avg ) / 5.0f )); } //Displays the slowests drivers int badId = 0 ; for ( int i = averages . Count - 1 ; i >= averages . Count - numberOfDriversToShow ; i --) { Button newButton = new Button (); ( int avg , string driver ) data = averages [ i ]; pnlSlowest . Controls . Add ( newButton ); newButton . Size = new Size ( pnlFastest . Width , maxUiSize ); newButton . Location = new Point ( 0 , badId * maxUiSize ); newButton . Text = data . driver ; newButton . FlatStyle = FlatStyle . Popup ; newButton . Name = data . driver + \"_slowest_\" + i ; newButton . Click += form1 . btnDriver_Click ; //We take the average time lost per lap newButton . Text += \" + \" + Reader . ConvertMsToTime ( Convert . ToInt32 ((( float ) data . avg - ( float ) averages [ 0 ]. avg ) / 5.0f )); badId ++; } } } /// <summary> /// Will add to the list of overtakes the different changes of position /// </summary> /// <param name=\"lsbResult\">The listbox containing all the infos</param> public void DisplayOvertakes ( ListBox lsbResult ) { //Note : This method SHOULD REALLY not do this but just return a string or a list of string with the new overtakes so the form can handle it as it wishes if ( LiveDriverDataLogs . Count > 1 ) { List < DriverData > oldList = LiveDriverDataLogs [ LiveDriverDataLogs . Count - 2 ]; List < DriverData > newList = LiveDriverDataLogs [ LiveDriverDataLogs . Count - 1 ]; for ( int i = 0 ; i < LiveDriverDataLogs [ LiveDriverDataLogs . Count - 1 ]. Count ; i ++) { if ( oldList [ i ]. Name != newList [ i ]. Name ) { //There has been a change in the standings for ( int y = 0 ; y < oldList . Count ; y ++) { if ( newList [ y ]. Name == oldList [ i ]. Name ) { //We found its new location if ( y > i ) { //The driver overtook someone lsbResult . Items . Add ( newList [ y ]. Name + \" climbed to \" + y ); } else { //The driver got overtook by someone lsbResult . Items . Add ( newList [ y ]. Name + \" fell to \" + y ); } } } } } } } /// <summary> /// Displays a messageBox containing the infos about a lap time /// </summary> /// <param name=\"driverName\">The name of the driver that has done the lapTime</param> /// <param name=\"Lap\">The number of the lap on wich the lapTime has been set (CAUTION ITS NOT THE RACING LAP ITS FROM THE DB)</param> /// <param name=\"LapTime\">The time (in ms) of the lap</param> public void DisplayLapTimeInfos ( string driverName , int Lap , string LapTime ) { List < int > sectors = Storage . GetSectorsFromLapTime ( driverName , Lap ); string message = \"Lap time infos\" + Environment . NewLine ; message += LapTime + Environment . NewLine ; if ( sectors . Count > 0 ) message += \"Sector 1 : \" + Reader . ConvertMsToTime ( sectors [ 0 ]) + Environment . NewLine ; if ( sectors . Count > 1 ) message += \"Sector 2 : \" + Reader . ConvertMsToTime ( sectors [ 1 ]) + Environment . NewLine ; if ( sectors . Count > 2 ) message += \"Sector 3 : \" + Reader . ConvertMsToTime ( sectors [ 2 ]) + Environment . NewLine ; MessageBox . Show ( message ); } /// <summary> /// Displays the live ranking with the names of the drivers and their gap to the leader in the right order /// </summary> /// <param name=\"pnl\">The control that will host all the new controls</param> /// <param name=\"form1\">The main form</param> public void DisplayLiveRanking ( Panel pnl , Main form1 ) { if ( LiveDriverDataLogs . Count > 0 ) { pnl . Controls . Clear (); //Gets the last item that should be the most recent data List < DriverData > liveData = LiveDriverDataLogs [ LiveDriverDataLogs . Count - 1 ]; Button [] buttons = new Button [ liveData . Count ]; Size buttonDimensions = new Size ( pnl . Width , pnl . Height / liveData . Count ); for ( int driverCount = 0 ; driverCount < liveData . Count ; driverCount ++) { Button newButton = new Button (); newButton . Size = buttonDimensions ; newButton . Location = new Point ( 0 , driverCount * buttonDimensions . Height ); newButton . FlatStyle = FlatStyle . Popup ; DriverData driver = liveData [ driverCount ]; if ( driver . Position == - 1 ) { //Its a DNF newButton . Enabled = false ; } if ( driver . Position > 1 ) { newButton . Text = driver . Name + \" +\" + Reader . ConvertMsToTime ( driver . GapToLeader ); } else { newButton . Text = driver . Name ; } newButton . Name = liveData [ driverCount ]. Name ; newButton . TextAlign = ContentAlignment . MiddleLeft ; newButton . FlatStyle = FlatStyle . Popup ; newButton . Click += form1 . btnDriver_Click ; buttons [ driverCount ] = newButton ; } //Note : It could be better to have this directly in the same loop foreach ( Button button in buttons ) { pnl . Controls . Add ( button ); } } } } }","title":"DataWrapper.cs"},{"location":"Code/DriverDrsWindow.html","text":"DriverDrsWindow.cs /// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DriverDrsWindow.cs /// Brief : Window containing DRS related method and infos /// Version : Beta 1.0 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 TrackTrends { public 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\" ; } /// <summary> /// Method that will decode the content of the window /// </summary> /// <returns>returns a boolean (true = DRS OPEN, false = DRS CLOSED)</returns> public override object DecodePng () { bool result = false ; //DEBUG //WindowImage.Save(\"./DRS/\"+rnd.Next(0,99999)+\".png\"); int greenValue = GetGreenPixels (); if ( EmptyDrsGreenValue == - 1 ) EmptyDrsGreenValue = greenValue ; if ( greenValue > EmptyDrsGreenValue + EmptyDrsGreenValue / 100 * 30 ) result = true ; return result ; } /// <summary> /// Method that will get the green pixel proportion in the image, this can be used to determin if the DRS has been actuated /// </summary> /// <returns>The number of clearely green pixels</returns> 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 ; } /// <summary> /// This method is used to lock on where exactly the DRS window is /// </summary> /// <returns>Returns a rectangle containing the DRS</returns> 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":"/// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DriverDrsWindow.cs /// Brief : Window containing DRS related method and infos /// Version : Beta 1.0 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 TrackTrends { public 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\" ; } /// <summary> /// Method that will decode the content of the window /// </summary> /// <returns>returns a boolean (true = DRS OPEN, false = DRS CLOSED)</returns> public override object DecodePng () { bool result = false ; //DEBUG //WindowImage.Save(\"./DRS/\"+rnd.Next(0,99999)+\".png\"); int greenValue = GetGreenPixels (); if ( EmptyDrsGreenValue == - 1 ) EmptyDrsGreenValue = greenValue ; if ( greenValue > EmptyDrsGreenValue + EmptyDrsGreenValue / 100 * 30 ) result = true ; return result ; } /// <summary> /// Method that will get the green pixel proportion in the image, this can be used to determin if the DRS has been actuated /// </summary> /// <returns>The number of clearely green pixels</returns> 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 ; } /// <summary> /// This method is used to lock on where exactly the DRS window is /// </summary> /// <returns>Returns a rectangle containing the DRS</returns> 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/DriverLapTimeWindow.html","text":"DriverLapTimeWindow.cs /// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DriverLapTimeWindow /// Brief : Window containing infos about the lap time of a driver /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using System.Drawing ; namespace TrackTrends { public 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 object DecodePng () { int result = GetTimeFromPng ( WindowImage , OcrImage . WindowType . LapTime , Engine ); return result ; } } }","title":"DriverLapTimeWindow.cs"},{"location":"Code/DriverLapTimeWindow.html#driverlaptimewindowcs","text":"/// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DriverLapTimeWindow /// Brief : Window containing infos about the lap time of a driver /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using System.Drawing ; namespace TrackTrends { public 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 object DecodePng () { int result = GetTimeFromPng ( WindowImage , OcrImage . WindowType . LapTime , Engine ); return result ; } } }","title":"DriverLapTimeWindow.cs"},{"location":"Code/DriverPositionWindow.html","text":"DriverPositionWindow.cs /// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DriverPositionWindow.cs /// Brief : Window containing infos about the position of a driver. /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using System.Drawing ; namespace TrackTrends { 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>An int representing the position of the driver (should be between 1 and 20 included)</returns> public override object DecodePng () { string ocrResult = 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":"/// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DriverPositionWindow.cs /// Brief : Window containing infos about the position of a driver. /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using System.Drawing ; namespace TrackTrends { 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>An int representing the position of the driver (should be between 1 and 20 included)</returns> public override object DecodePng () { string ocrResult = GetStringFromPng ( WindowImage , Engine , \"0123456789\" ); int position ; try { position = Convert . ToInt32 ( ocrResult ); } catch { position = - 1 ; } return position ; } } }","title":"DriverPositionWindow.cs"},{"location":"Code/DriverTyresWindow.html","text":"DriverTyresWindow.cs /// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DriverTyresWindow.cs /// Brief : Window containing infos about a driver's tyre /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using System.Drawing ; namespace TrackTrends { public class DriverTyresWindow : Window { private static Random rnd = new Random (); //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 ( 0 xff , 0 x00 , 0 x00 ); public static Color MEDIUM_TYRE_COLOR = Color . FromArgb ( 0 xf5 , 0 xbf , 0 x00 ); public static Color HARD_TYRE_COLOR = Color . FromArgb ( 0 xa4 , 0 xa5 , 0 xa8 ); public static Color INTER_TYRE_COLOR = Color . FromArgb ( 0 x00 , 0 xa4 , 0 x2e ); public static Color WET_TYRE_COLOR = Color . FromArgb ( 0 x27 , 0 x60 , 0 xa6 ); public static Color EMPTY_COLOR = Color . FromArgb ( 0 x20 , 0 x20 , 0 x20 ); 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 object DecodePng () { return 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 Tyre GetTyreInfos () { //DEBUG //WindowImage.Save(\"./Tyre/raw_\"+rnd.Next(0,99999)+\".png\"); Bitmap tyreZone = GetSmallBitmapFromBigOne ( WindowImage , FindTyreZone ()); Tyre . Type type = Tyre . Type . Undefined ; type = GetTyreTypeFromColor ( OcrImage . GetAvgColorFromBitmap ( tyreZone )); int laps = - 1 ; string number = 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 ; } //71 is the most laps an f1 race is ever going to have (mexico) so any more would be considered as bad (and remember you cant go trough a full race without making at least one pitstop) if ( laps > 75 ) laps = 0 ; 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 ( 0 x50 , 0 x50 , 0 x50 ); Color currentColor = Color . FromArgb ( 0 , 0 , 0 ); //25F 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":"/// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : DriverTyresWindow.cs /// Brief : Window containing infos about a driver's tyre /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Linq ; using System.Text ; using System.Threading.Tasks ; using System.Drawing ; namespace TrackTrends { public class DriverTyresWindow : Window { private static Random rnd = new Random (); //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 ( 0 xff , 0 x00 , 0 x00 ); public static Color MEDIUM_TYRE_COLOR = Color . FromArgb ( 0 xf5 , 0 xbf , 0 x00 ); public static Color HARD_TYRE_COLOR = Color . FromArgb ( 0 xa4 , 0 xa5 , 0 xa8 ); public static Color INTER_TYRE_COLOR = Color . FromArgb ( 0 x00 , 0 xa4 , 0 x2e ); public static Color WET_TYRE_COLOR = Color . FromArgb ( 0 x27 , 0 x60 , 0 xa6 ); public static Color EMPTY_COLOR = Color . FromArgb ( 0 x20 , 0 x20 , 0 x20 ); 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 object DecodePng () { return 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 Tyre GetTyreInfos () { //DEBUG //WindowImage.Save(\"./Tyre/raw_\"+rnd.Next(0,99999)+\".png\"); Bitmap tyreZone = GetSmallBitmapFromBigOne ( WindowImage , FindTyreZone ()); Tyre . Type type = Tyre . Type . Undefined ; type = GetTyreTypeFromColor ( OcrImage . GetAvgColorFromBitmap ( tyreZone )); int laps = - 1 ; string number = 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 ; } //71 is the most laps an f1 race is ever going to have (mexico) so any more would be considered as bad (and remember you cant go trough a full race without making at least one pitstop) if ( laps > 75 ) laps = 0 ; 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 ( 0 x50 , 0 x50 , 0 x50 ); Color currentColor = Color . FromArgb ( 0 , 0 , 0 ); //25F 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 /// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : OcrImage.cs /// Brief : Class containing all the methods used to enhance images for OCR /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Threading.Tasks ; using System.Drawing ; using System.Drawing.Drawing2D ; using System.Drawing.Imaging ; namespace TrackTrends { 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 ( 0 x50 , 0 x50 , 0 x50 ); Bitmap InputBitmap ; Random rnd = new Random (); 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 (); //Note : If you plan to activate all the comments that I used to debug the OCR I would advise to make sure that the debug folder exists switch ( type ) { case WindowType . Gap : //outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @\"\\raw_\" + id + \".png\"); outputBitmap = Tresholding ( outputBitmap , 165 ); //outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @\"\\treshold_\" + id + \".png\"); outputBitmap = Resize ( outputBitmap , 2 ); //outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @\"\\resize_\" + id + \".png\"); outputBitmap = Dilatation ( outputBitmap , 1 ); //outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @\"\\Final_dilatation_\" + id + \".png\"); break ; case WindowType . Sector : //outputBitmap.Save(Window.SECTOR1_DEBUG_FOLDER + @\"\\raw_\" + id + \".png\"); outputBitmap = VanishOxyAction ( outputBitmap ); //outputBitmap.Save(Window.SECTOR1_DEBUG_FOLDER + @\"\\vanish_\" + id + \".png\"); outputBitmap = Tresholding ( outputBitmap , 150 ); //outputBitmap.Save(Window.SECTOR1_DEBUG_FOLDER + @\"\\Final_treshold_\" + id + \".png\"); break ; case WindowType . LapTime : //outputBitmap.Save(Window.LAPTIME_DEBUG_FOLDER + @\"\\raw_\" + id + \".png\"); outputBitmap = Tresholding ( outputBitmap , 185 ); //outputBitmap.Save(Window.LAPTIME_DEBUG_FOLDER + @\"\\Treshold_\" + id + \".png\"); outputBitmap = SobelEdgeDetection ( outputBitmap ); //outputBitmap.Save(Window.LAPTIME_DEBUG_FOLDER + @\"\\SobelDetection_\" + id + \".png\"); break ; case WindowType . Text : //outputBitmap.Save(Window.STRING_DEBUG_FOLDER + @\"\\raw_\" + id + \".png\"); outputBitmap = Tresholding ( outputBitmap , 165 ); //outputBitmap.Save(Window.STRING_DEBUG_FOLDER + @\"\\Final_treshold_\" + id + \".png\"); break ; case WindowType . Tyre : //outputBitmap.Save(Window.TYRE_DEBUG_FOLDER + @\"\\raw_\" + id + \".png\"); outputBitmap = RemoveUseless ( outputBitmap ); //outputBitmap.Save(Window.TYRE_DEBUG_FOLDER + @\"\\uselessRemoved_\" + id + \".png\"); outputBitmap = Dilatation ( outputBitmap , 1 ); //outputBitmap.Save(Window.TYRE_DEBUG_FOLDER + @\"\\Final_dilatation_\" + id + \".png\"); break ; default : outputBitmap = Tresholding ( outputBitmap , 165 ); outputBitmap = Resize ( outputBitmap , 2 ); 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 . ReadOnly , inputBitmap . PixelFormat ); int bytesPerPixel = Bitmap . GetPixelFormatSize ( inputBitmap . PixelFormat ) / 8 ; Bitmap resultBitmap = new Bitmap ( inputBitmap . Width , inputBitmap . Height ); unsafe { byte * inputPtr = ( byte *) bmpData . Scan0 . ToPointer (); byte * resultPtr = ( byte *) resultBitmap . LockBits ( rect , ImageLockMode . WriteOnly , resultBitmap . PixelFormat ). Scan0 . ToPointer (); Parallel . For ( 0 , inputBitmap . Height , y => { byte * currentLine = inputPtr + ( y * bmpData . Stride ); byte * resultLine = resultPtr + ( y * resultBitmap . Width * bytesPerPixel ); for ( int x = 0 ; x < inputBitmap . Width ; x ++) { byte * inputPixel = currentLine + ( x * bytesPerPixel ); byte * resultPixel = resultLine + ( x * bytesPerPixel ); byte blue = inputPixel [ 0 ]; byte green = inputPixel [ 1 ]; byte red = inputPixel [ 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 ); //This is not a proper treshold method but it is helping the sobel edge detection if ( gray <= F1TV_BACKGROUND_TRESHOLD . R ) { resultPixel [ 0 ] = resultPixel [ 1 ] = resultPixel [ 2 ] = 0 ; } else { resultPixel [ 0 ] = resultPixel [ 1 ] = resultPixel [ 2 ] = ( byte ) gray ; } } }); resultBitmap . UnlockBits ( resultBitmap . LockBits ( rect , ImageLockMode . WriteOnly , resultBitmap . PixelFormat )); } inputBitmap . UnlockBits ( bmpData ); return resultBitmap ; } /// <summary> /// Method that uses the Sobel Edge detection to outline the edges of the characters to help with the OCR /// </summary> /// <param name=\"grayscaleImage\">The image with the sobel edge detection used</param> /// <returns></returns> private Bitmap SobelEdgeDetection ( Bitmap grayscaleImage ) { // Create a new bitmap for the edges Bitmap edgesImage = new Bitmap ( grayscaleImage . Width , grayscaleImage . Height ); // Define the Sobel operators // Its just a matrix that we will use on the all image int [,] sobelX = { { - 1 , 0 , 1 }, { - 2 , 0 , 2 }, { - 1 , 0 , 1 } }; int [,] sobelY = { { - 1 , - 2 , - 1 }, { 0 , 0 , 0 }, { 1 , 2 , 1 } }; // Apply the Sobel operators and normalize the gradients // NOTE: I dont know how easy or hard it would be to make this paralel but it could be a good idea to do so if possible. //Parallel.For(1, grayscaleImage.Height - 1, y => for ( int y = 1 ; y < grayscaleImage . Height - 1 ; y ++) { for ( int x = 1 ; x < grayscaleImage . Width - 1 ; x ++) { int gradientX = CalculateGradient ( grayscaleImage , sobelX , x , y ); int gradientY = CalculateGradient ( grayscaleImage , sobelY , x , y ); int gradient = ( int ) Math . Sqrt ( gradientX * gradientX + gradientY * gradientY ); // Normalize the gradient value // In some rare cases the value can exceed 255 so we limit it with the Math.Min method gradient = Math . Min ( 255 , Math . Max ( 0 , gradient )); edgesImage . SetPixel ( x , y , Color . FromArgb ( gradient , gradient , gradient )); } } //); return edgesImage ; } /// <summary> /// Method that's here to be used by the sobel edge detection method (Chat GPT has been used for parts of this method) /// </summary> /// <param name=\"grayscaleImage\">The input image with the grayscale processing already done</param> /// <param name=\"sobelOperator\">The matrix to apply</param> /// <param name=\"x\"></param> /// <param name=\"y\"></param> /// <returns>Returns the processed gradient</returns> private int CalculateGradient ( Bitmap grayscaleImage , int [,] sobelOperator , int x , int y ) { int gradient = 0 ; for ( int j = - 1 ; j <= 1 ; j ++) { for ( int i = - 1 ; i <= 1 ; i ++) { int pixelX = grayscaleImage . GetPixel ( x + i , y + j ). R ; gradient += sobelOperator [ j + 1 , i + 1 ] * pixelX ; } } return gradient ; } /// <summary> /// Method that is used to whiten an image. Ignore the funny name. Its used to prevent colored text to trouble the OCR when it uses grayscaling /// </summary> /// <param name=\"inputBitmap\">The bitmap to vanish</param> /// <returns></returns> public Bitmap VanishOxyAction ( Bitmap inputBitmap ) { unsafe { BitmapData bitmapData = inputBitmap . LockBits ( new Rectangle ( 0 , 0 , inputBitmap . Width , inputBitmap . Height ), ImageLockMode . ReadWrite , inputBitmap . PixelFormat ); int bytesPerPixel = System . Drawing . Bitmap . GetPixelFormatSize ( inputBitmap . PixelFormat ) / 8 ; int heightInPixels = bitmapData . Height ; int widthInBytes = bitmapData . Width * bytesPerPixel ; byte * PtrFirstPixel = ( byte *) bitmapData . Scan0 ; Parallel . For ( 0 , heightInPixels , y => { byte * currentLine = PtrFirstPixel + ( y * bitmapData . Stride ); for ( int x = 0 ; x < widthInBytes ; x = x + bytesPerPixel ) { int blue = currentLine [ x ]; int green = currentLine [ x + 1 ]; int red = currentLine [ x + 2 ]; int max = Math . Max ( Math . Max ( blue , green ), red ); if ( max > 255 / 3 ) max = 255 ; currentLine [ x ] = currentLine [ x + 1 ] = currentLine [ x + 2 ] = ( byte ) max ; } }); inputBitmap . UnlockBits ( bitmapData ); } 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 (); 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 ) 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 ] = 0 xFF ; pixel [ 1 ] = 0 xFF ; pixel [ 2 ] = 0 xFF ; } } //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 + 25 || G >= F1TV_BACKGROUND_TRESHOLD . G + 25 || B >= F1TV_BACKGROUND_TRESHOLD . B + 25 ) { pixel [ 0 ] = 0 xFF ; pixel [ 1 ] = 0 xFF ; pixel [ 2 ] = 0 xFF ; } } } 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 , Math . Min ( Convert . ToInt32 (( float ) totR / ( float ) totPixels ), 255 ), Math . Min ( Convert . ToInt32 (( float ) totG / ( float ) totPixels ), 255 ), Math . Min ( Convert . ToInt32 (( float ) totB / ( float ) totPixels ), 255 )); } /// <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":"/// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : OcrImage.cs /// Brief : Class containing all the methods used to enhance images for OCR /// Version : Beta 1.0 using System ; using System.Collections.Generic ; using System.Threading.Tasks ; using System.Drawing ; using System.Drawing.Drawing2D ; using System.Drawing.Imaging ; namespace TrackTrends { 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 ( 0 x50 , 0 x50 , 0 x50 ); Bitmap InputBitmap ; Random rnd = new Random (); 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 (); //Note : If you plan to activate all the comments that I used to debug the OCR I would advise to make sure that the debug folder exists switch ( type ) { case WindowType . Gap : //outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @\"\\raw_\" + id + \".png\"); outputBitmap = Tresholding ( outputBitmap , 165 ); //outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @\"\\treshold_\" + id + \".png\"); outputBitmap = Resize ( outputBitmap , 2 ); //outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @\"\\resize_\" + id + \".png\"); outputBitmap = Dilatation ( outputBitmap , 1 ); //outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @\"\\Final_dilatation_\" + id + \".png\"); break ; case WindowType . Sector : //outputBitmap.Save(Window.SECTOR1_DEBUG_FOLDER + @\"\\raw_\" + id + \".png\"); outputBitmap = VanishOxyAction ( outputBitmap ); //outputBitmap.Save(Window.SECTOR1_DEBUG_FOLDER + @\"\\vanish_\" + id + \".png\"); outputBitmap = Tresholding ( outputBitmap , 150 ); //outputBitmap.Save(Window.SECTOR1_DEBUG_FOLDER + @\"\\Final_treshold_\" + id + \".png\"); break ; case WindowType . LapTime : //outputBitmap.Save(Window.LAPTIME_DEBUG_FOLDER + @\"\\raw_\" + id + \".png\"); outputBitmap = Tresholding ( outputBitmap , 185 ); //outputBitmap.Save(Window.LAPTIME_DEBUG_FOLDER + @\"\\Treshold_\" + id + \".png\"); outputBitmap = SobelEdgeDetection ( outputBitmap ); //outputBitmap.Save(Window.LAPTIME_DEBUG_FOLDER + @\"\\SobelDetection_\" + id + \".png\"); break ; case WindowType . Text : //outputBitmap.Save(Window.STRING_DEBUG_FOLDER + @\"\\raw_\" + id + \".png\"); outputBitmap = Tresholding ( outputBitmap , 165 ); //outputBitmap.Save(Window.STRING_DEBUG_FOLDER + @\"\\Final_treshold_\" + id + \".png\"); break ; case WindowType . Tyre : //outputBitmap.Save(Window.TYRE_DEBUG_FOLDER + @\"\\raw_\" + id + \".png\"); outputBitmap = RemoveUseless ( outputBitmap ); //outputBitmap.Save(Window.TYRE_DEBUG_FOLDER + @\"\\uselessRemoved_\" + id + \".png\"); outputBitmap = Dilatation ( outputBitmap , 1 ); //outputBitmap.Save(Window.TYRE_DEBUG_FOLDER + @\"\\Final_dilatation_\" + id + \".png\"); break ; default : outputBitmap = Tresholding ( outputBitmap , 165 ); outputBitmap = Resize ( outputBitmap , 2 ); 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 . ReadOnly , inputBitmap . PixelFormat ); int bytesPerPixel = Bitmap . GetPixelFormatSize ( inputBitmap . PixelFormat ) / 8 ; Bitmap resultBitmap = new Bitmap ( inputBitmap . Width , inputBitmap . Height ); unsafe { byte * inputPtr = ( byte *) bmpData . Scan0 . ToPointer (); byte * resultPtr = ( byte *) resultBitmap . LockBits ( rect , ImageLockMode . WriteOnly , resultBitmap . PixelFormat ). Scan0 . ToPointer (); Parallel . For ( 0 , inputBitmap . Height , y => { byte * currentLine = inputPtr + ( y * bmpData . Stride ); byte * resultLine = resultPtr + ( y * resultBitmap . Width * bytesPerPixel ); for ( int x = 0 ; x < inputBitmap . Width ; x ++) { byte * inputPixel = currentLine + ( x * bytesPerPixel ); byte * resultPixel = resultLine + ( x * bytesPerPixel ); byte blue = inputPixel [ 0 ]; byte green = inputPixel [ 1 ]; byte red = inputPixel [ 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 ); //This is not a proper treshold method but it is helping the sobel edge detection if ( gray <= F1TV_BACKGROUND_TRESHOLD . R ) { resultPixel [ 0 ] = resultPixel [ 1 ] = resultPixel [ 2 ] = 0 ; } else { resultPixel [ 0 ] = resultPixel [ 1 ] = resultPixel [ 2 ] = ( byte ) gray ; } } }); resultBitmap . UnlockBits ( resultBitmap . LockBits ( rect , ImageLockMode . WriteOnly , resultBitmap . PixelFormat )); } inputBitmap . UnlockBits ( bmpData ); return resultBitmap ; } /// <summary> /// Method that uses the Sobel Edge detection to outline the edges of the characters to help with the OCR /// </summary> /// <param name=\"grayscaleImage\">The image with the sobel edge detection used</param> /// <returns></returns> private Bitmap SobelEdgeDetection ( Bitmap grayscaleImage ) { // Create a new bitmap for the edges Bitmap edgesImage = new Bitmap ( grayscaleImage . Width , grayscaleImage . Height ); // Define the Sobel operators // Its just a matrix that we will use on the all image int [,] sobelX = { { - 1 , 0 , 1 }, { - 2 , 0 , 2 }, { - 1 , 0 , 1 } }; int [,] sobelY = { { - 1 , - 2 , - 1 }, { 0 , 0 , 0 }, { 1 , 2 , 1 } }; // Apply the Sobel operators and normalize the gradients // NOTE: I dont know how easy or hard it would be to make this paralel but it could be a good idea to do so if possible. //Parallel.For(1, grayscaleImage.Height - 1, y => for ( int y = 1 ; y < grayscaleImage . Height - 1 ; y ++) { for ( int x = 1 ; x < grayscaleImage . Width - 1 ; x ++) { int gradientX = CalculateGradient ( grayscaleImage , sobelX , x , y ); int gradientY = CalculateGradient ( grayscaleImage , sobelY , x , y ); int gradient = ( int ) Math . Sqrt ( gradientX * gradientX + gradientY * gradientY ); // Normalize the gradient value // In some rare cases the value can exceed 255 so we limit it with the Math.Min method gradient = Math . Min ( 255 , Math . Max ( 0 , gradient )); edgesImage . SetPixel ( x , y , Color . FromArgb ( gradient , gradient , gradient )); } } //); return edgesImage ; } /// <summary> /// Method that's here to be used by the sobel edge detection method (Chat GPT has been used for parts of this method) /// </summary> /// <param name=\"grayscaleImage\">The input image with the grayscale processing already done</param> /// <param name=\"sobelOperator\">The matrix to apply</param> /// <param name=\"x\"></param> /// <param name=\"y\"></param> /// <returns>Returns the processed gradient</returns> private int CalculateGradient ( Bitmap grayscaleImage , int [,] sobelOperator , int x , int y ) { int gradient = 0 ; for ( int j = - 1 ; j <= 1 ; j ++) { for ( int i = - 1 ; i <= 1 ; i ++) { int pixelX = grayscaleImage . GetPixel ( x + i , y + j ). R ; gradient += sobelOperator [ j + 1 , i + 1 ] * pixelX ; } } return gradient ; } /// <summary> /// Method that is used to whiten an image. Ignore the funny name. Its used to prevent colored text to trouble the OCR when it uses grayscaling /// </summary> /// <param name=\"inputBitmap\">The bitmap to vanish</param> /// <returns></returns> public Bitmap VanishOxyAction ( Bitmap inputBitmap ) { unsafe { BitmapData bitmapData = inputBitmap . LockBits ( new Rectangle ( 0 , 0 , inputBitmap . Width , inputBitmap . Height ), ImageLockMode . ReadWrite , inputBitmap . PixelFormat ); int bytesPerPixel = System . Drawing . Bitmap . GetPixelFormatSize ( inputBitmap . PixelFormat ) / 8 ; int heightInPixels = bitmapData . Height ; int widthInBytes = bitmapData . Width * bytesPerPixel ; byte * PtrFirstPixel = ( byte *) bitmapData . Scan0 ; Parallel . For ( 0 , heightInPixels , y => { byte * currentLine = PtrFirstPixel + ( y * bitmapData . Stride ); for ( int x = 0 ; x < widthInBytes ; x = x + bytesPerPixel ) { int blue = currentLine [ x ]; int green = currentLine [ x + 1 ]; int red = currentLine [ x + 2 ]; int max = Math . Max ( Math . Max ( blue , green ), red ); if ( max > 255 / 3 ) max = 255 ; currentLine [ x ] = currentLine [ x + 1 ] = currentLine [ x + 2 ] = ( byte ) max ; } }); inputBitmap . UnlockBits ( bitmapData ); } 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 (); 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 ) 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 ] = 0 xFF ; pixel [ 1 ] = 0 xFF ; pixel [ 2 ] = 0 xFF ; } } //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 + 25 || G >= F1TV_BACKGROUND_TRESHOLD . G + 25 || B >= F1TV_BACKGROUND_TRESHOLD . B + 25 ) { pixel [ 0 ] = 0 xFF ; pixel [ 1 ] = 0 xFF ; pixel [ 2 ] = 0 xFF ; } } } 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 , Math . Min ( Convert . ToInt32 (( float ) totR / ( float ) totPixels ), 255 ), Math . Min ( Convert . ToInt32 (( float ) totG / ( float ) totPixels ), 255 ), Math . Min ( Convert . ToInt32 (( float ) totB / ( float ) totPixels ), 255 )); } /// <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/Reader.html","text":"Reader.cs /// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : Reader.cs /// Brief : Class used to Read the config file for the OCR /// Version : Beta 1.0 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 TrackTrends { public class Reader { const int NUMBER_OF_DRIVERS = 20 ; public List < string > Drivers ; public List < Zone > MainZones ; private SqliteStorage _storage ; private List < DriverData >[] DriverDataLogs = new List < DriverData >[ NUMBER_OF_DRIVERS ]; private int [] DriverLaps = new int [ NUMBER_OF_DRIVERS ]; public SqliteStorage Storage { get => _storage ; private set => _storage = value ; } public Reader ( string configFile , Bitmap image , bool loadOCR = true ) { Storage = new SqliteStorage (); 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 List < Zone > Load ( Bitmap image , string configFilePath , ref List < string > driverListToFill , bool LoadOCR ) { // Note : You may wonder why in the H... I have all the zones and windows stored in a JSON file and not just for example the first and the last // Its because they are not perfectly aligned to each others and every zone has his own alignement to the main image List < Zone > mainZones = new List < Zone >(); Bitmap fullImage = image ; Zone mainZone ; for ( int i = 0 ; i < NUMBER_OF_DRIVERS ; i ++) { DriverDataLogs [ i ] = new List < DriverData >(); DriverLaps [ i ] = 0 ; } try { string jsonString = File . ReadAllText ( configFilePath ); JsonDocument document = JsonDocument . Parse ( jsonString ); JsonElement root = document . RootElement ; mainZones = new List < Zone >(); driverListToFill = new List < string >(); JsonElement main = root . GetProperty ( \"Main\" ); int x = main . GetProperty ( \"x\" ). GetInt32 (); int y = main . GetProperty ( \"y\" ). GetInt32 (); int width = main . GetProperty ( \"width\" ). GetInt32 (); int height = main . GetProperty ( \"height\" ). GetInt32 (); mainZone = new Zone ( fullImage , new Rectangle ( x , y , width , height ), \"Main\" ); mainZone . ResetWindows (); mainZone . ResetZones (); JsonElement driverZones = main . GetProperty ( \"DriverZones\" ); foreach ( JsonElement driverZoneElement in driverZones . EnumerateArray ()) { string name = driverZoneElement . GetProperty ( \"name\" ). GetString (); int driverX = driverZoneElement . GetProperty ( \"x\" ). GetInt32 () + mainZone . Bounds . X ; int driverY = driverZoneElement . GetProperty ( \"y\" ). GetInt32 () + mainZone . Bounds . Y ; int driverWidth = driverZoneElement . GetProperty ( \"width\" ). GetInt32 (); int driverHeight = driverZoneElement . GetProperty ( \"height\" ). GetInt32 (); Zone driverZone = new Zone ( fullImage , new Rectangle ( driverX , driverY , driverWidth , driverHeight ), \"Driver\" ); JsonElement windowsElement = driverZoneElement . GetProperty ( \"Windows\" ); //string[] windowNames = new string[] { \"Position\",\"GapToLeader\",\"LapTime\",\"DRS\",\"Tyres\",\"Name\",\"Sector1\",\"Sector2\",\"Sector3\" }; foreach ( JsonElement windowElement in windowsElement . EnumerateArray ()) { //Position JsonElement posEl = windowElement . GetProperty ( \"Position\" ); DriverPositionWindow positionWindow = new DriverPositionWindow ( driverZone . ZoneImage , new Rectangle ( posEl . GetProperty ( \"x\" ). GetInt32 (), posEl . GetProperty ( \"y\" ). GetInt32 (), posEl . GetProperty ( \"width\" ). GetInt32 (), posEl . GetProperty ( \"height\" ). GetInt32 ()), LoadOCR ); //GapToLeader JsonElement gapEl = windowElement . GetProperty ( \"GapToLeader\" ); DriverGapToLeaderWindow gapWindow = new DriverGapToLeaderWindow ( driverZone . ZoneImage , new Rectangle ( gapEl . GetProperty ( \"x\" ). GetInt32 (), gapEl . GetProperty ( \"y\" ). GetInt32 (), gapEl . GetProperty ( \"width\" ). GetInt32 (), gapEl . GetProperty ( \"height\" ). GetInt32 ()), LoadOCR ); //LapTime JsonElement lapEl = windowElement . GetProperty ( \"LapTime\" ); DriverLapTimeWindow lapWindow = new DriverLapTimeWindow ( driverZone . ZoneImage , new Rectangle ( lapEl . GetProperty ( \"x\" ). GetInt32 (), lapEl . GetProperty ( \"y\" ). GetInt32 (), lapEl . GetProperty ( \"width\" ). GetInt32 (), lapEl . GetProperty ( \"height\" ). GetInt32 ()), LoadOCR ); //DRS JsonElement drsEl = windowElement . GetProperty ( \"DRS\" ); DriverDrsWindow drsWindow = new DriverDrsWindow ( driverZone . ZoneImage , new Rectangle ( drsEl . GetProperty ( \"x\" ). GetInt32 (), drsEl . GetProperty ( \"y\" ). GetInt32 (), drsEl . GetProperty ( \"width\" ). GetInt32 (), drsEl . GetProperty ( \"height\" ). GetInt32 ()), LoadOCR ); //Tyre JsonElement tyresEl = windowElement . GetProperty ( \"Tyres\" ); DriverTyresWindow tyreWindow = new DriverTyresWindow ( driverZone . ZoneImage , new Rectangle ( tyresEl . GetProperty ( \"x\" ). GetInt32 (), tyresEl . GetProperty ( \"y\" ). GetInt32 (), tyresEl . GetProperty ( \"width\" ). GetInt32 (), tyresEl . GetProperty ( \"height\" ). GetInt32 ()), LoadOCR ); //Name JsonElement nameEl = windowElement . GetProperty ( \"Name\" ); DriverNameWindow nameWindow = new DriverNameWindow ( driverZone . ZoneImage , new Rectangle ( nameEl . GetProperty ( \"x\" ). GetInt32 (), nameEl . GetProperty ( \"y\" ). GetInt32 (), nameEl . GetProperty ( \"width\" ). GetInt32 (), nameEl . GetProperty ( \"height\" ). GetInt32 ()), LoadOCR ); //Sector1 JsonElement sec1El = windowElement . GetProperty ( \"Sector1\" ); DriverSectorWindow sec1Window = new DriverSectorWindow ( driverZone . ZoneImage , new Rectangle ( sec1El . GetProperty ( \"x\" ). GetInt32 (), sec1El . GetProperty ( \"y\" ). GetInt32 (), sec1El . GetProperty ( \"width\" ). GetInt32 (), sec1El . GetProperty ( \"height\" ). GetInt32 ()), 1 , LoadOCR ); //Sector2 JsonElement sec2El = windowElement . GetProperty ( \"Sector2\" ); DriverSectorWindow sec2Window = new DriverSectorWindow ( driverZone . ZoneImage , new Rectangle ( sec2El . GetProperty ( \"x\" ). GetInt32 (), sec2El . GetProperty ( \"y\" ). GetInt32 (), sec2El . GetProperty ( \"width\" ). GetInt32 (), sec2El . GetProperty ( \"height\" ). GetInt32 ()), 2 , LoadOCR ); //Sector3 JsonElement sec3El = windowElement . GetProperty ( \"Sector3\" ); DriverSectorWindow sec3Window = new DriverSectorWindow ( driverZone . ZoneImage , new Rectangle ( sec3El . GetProperty ( \"x\" ). GetInt32 (), sec3El . GetProperty ( \"y\" ). GetInt32 (), sec3El . GetProperty ( \"width\" ). GetInt32 (), sec3El . GetProperty ( \"height\" ). GetInt32 ()), 3 , LoadOCR ); driverZone . AddWindow ( positionWindow ); driverZone . AddWindow ( gapWindow ); driverZone . AddWindow ( lapWindow ); driverZone . AddWindow ( drsWindow ); driverZone . AddWindow ( tyreWindow ); driverZone . AddWindow ( nameWindow ); driverZone . AddWindow ( sec1Window ); driverZone . AddWindow ( sec2Window ); driverZone . AddWindow ( sec3Window ); } mainZone . AddZone ( driverZone ); } JsonElement driversElement = main . GetProperty ( \"Drivers\" ); foreach ( JsonElement driverElement in driversElement . EnumerateArray ()) { string driverName = driverElement . GetString (); driverListToFill . Add ( driverName ); Storage . AddDriver ( driverName ); } mainZones . Add ( mainZone ); } catch ( IOException ex ) { MessageBox . Show ( \"Error reading JSON file: \" + ex . Message ); } catch ( JsonException ex ) { MessageBox . Show ( \"Invalid JSON format: \" + ex . Message ); } int driverID = 0 ; foreach ( Zone z in mainZones [ 0 ]. Zones ) { driverID ++; z . ZoneImage . Save ( \"LoadedDriver\" + driverID + \".png\" ); } 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 List < DriverData > Decode ( List < Zone > mainZones , List < string > drivers ) { List < DriverData > mainResults = new List < DriverData >(); //Decode for ( int mainZoneId = 0 ; mainZoneId < mainZones . Count ; mainZoneId ++) { switch ( mainZoneId ) { case 0 : //object lockObject = new object(); //Main Zone Parallel . For ( 0 , mainZones [ mainZoneId ]. Zones . Count , async i => //for (int i = 0; i < mainZones[mainZoneId].Zones.Count; i++) { DriverData data = mainZones [ mainZoneId ]. Zones [ i ]. Decode ( new List < string >( drivers )); mainResults . Add ( data ); DriverDataLogs [ i ]. Add ( data ); if ( data . Position != - 1 && DriverDataLogs [ i ]. Count > 1 ) { //Tries to fix the tyres if ( data . CurrentTyre . NumberOfLaps > DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. CurrentTyre . NumberOfLaps + 3 ) data . CurrentTyre . NumberOfLaps = DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. CurrentTyre . NumberOfLaps + 1 ; //Checking if its a new lap //If the third sector is filled but it was'nt the last time, then it means that a new Lap has been started //Lap detection can be f***ed if the OCR takes so much time that an entire sector can be raced without us knowing. if ( DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 1 ]. Sector3 != 0 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. Sector3 == 0 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. Position != - 1 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 1 ]. Position != - 1 ) { DriverData stats = new DriverData (); stats = DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 1 ]; DriverLaps [ i ]++; Storage . AddDriverStat ( stats , DriverLaps [ i ]); } //Checking if its a pitstop //Forget this the best way to know if a tyre has been changed is if the number of laps is zero if ( data . CurrentTyre . Coumpound != Tyre . Type . Undefined && data . CurrentTyre . NumberOfLaps == 0 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. CurrentTyre . NumberOfLaps != 0 ) { Storage . AddPitstop ( data . Name , DriverLaps [ i ] - 1 , data . CurrentTyre . Coumpound . ToString ()); //Driver laps -1 because it would take AT LEAST one lap for this program to detect a pitstop } } DriverDataLogs [ i ]. Add ( data ); }); break ; //Next there could be a Title Zone and TrackInfoZone } } //mainResults = mainResults.OrderBy(driver => driver.Position >= 0).ThenBy(driver => driver.Position).ToList(); mainResults = mainResults . OrderBy ( driver => driver . Position ). ToList (); return mainResults ; } /// <summary> /// Changes the image in all of the zones wich then will do the same for theyre own subzones and windows /// </summary> /// <param name=\"Image\">The new Image from the F1TV data channel</param> public void ChangeImage ( Bitmap Image ) { foreach ( Zone z in MainZones ) { z . Image = Image ; } } /// <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":"/// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : Reader.cs /// Brief : Class used to Read the config file for the OCR /// Version : Beta 1.0 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 TrackTrends { public class Reader { const int NUMBER_OF_DRIVERS = 20 ; public List < string > Drivers ; public List < Zone > MainZones ; private SqliteStorage _storage ; private List < DriverData >[] DriverDataLogs = new List < DriverData >[ NUMBER_OF_DRIVERS ]; private int [] DriverLaps = new int [ NUMBER_OF_DRIVERS ]; public SqliteStorage Storage { get => _storage ; private set => _storage = value ; } public Reader ( string configFile , Bitmap image , bool loadOCR = true ) { Storage = new SqliteStorage (); 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 List < Zone > Load ( Bitmap image , string configFilePath , ref List < string > driverListToFill , bool LoadOCR ) { // Note : You may wonder why in the H... I have all the zones and windows stored in a JSON file and not just for example the first and the last // Its because they are not perfectly aligned to each others and every zone has his own alignement to the main image List < Zone > mainZones = new List < Zone >(); Bitmap fullImage = image ; Zone mainZone ; for ( int i = 0 ; i < NUMBER_OF_DRIVERS ; i ++) { DriverDataLogs [ i ] = new List < DriverData >(); DriverLaps [ i ] = 0 ; } try { string jsonString = File . ReadAllText ( configFilePath ); JsonDocument document = JsonDocument . Parse ( jsonString ); JsonElement root = document . RootElement ; mainZones = new List < Zone >(); driverListToFill = new List < string >(); JsonElement main = root . GetProperty ( \"Main\" ); int x = main . GetProperty ( \"x\" ). GetInt32 (); int y = main . GetProperty ( \"y\" ). GetInt32 (); int width = main . GetProperty ( \"width\" ). GetInt32 (); int height = main . GetProperty ( \"height\" ). GetInt32 (); mainZone = new Zone ( fullImage , new Rectangle ( x , y , width , height ), \"Main\" ); mainZone . ResetWindows (); mainZone . ResetZones (); JsonElement driverZones = main . GetProperty ( \"DriverZones\" ); foreach ( JsonElement driverZoneElement in driverZones . EnumerateArray ()) { string name = driverZoneElement . GetProperty ( \"name\" ). GetString (); int driverX = driverZoneElement . GetProperty ( \"x\" ). GetInt32 () + mainZone . Bounds . X ; int driverY = driverZoneElement . GetProperty ( \"y\" ). GetInt32 () + mainZone . Bounds . Y ; int driverWidth = driverZoneElement . GetProperty ( \"width\" ). GetInt32 (); int driverHeight = driverZoneElement . GetProperty ( \"height\" ). GetInt32 (); Zone driverZone = new Zone ( fullImage , new Rectangle ( driverX , driverY , driverWidth , driverHeight ), \"Driver\" ); JsonElement windowsElement = driverZoneElement . GetProperty ( \"Windows\" ); //string[] windowNames = new string[] { \"Position\",\"GapToLeader\",\"LapTime\",\"DRS\",\"Tyres\",\"Name\",\"Sector1\",\"Sector2\",\"Sector3\" }; foreach ( JsonElement windowElement in windowsElement . EnumerateArray ()) { //Position JsonElement posEl = windowElement . GetProperty ( \"Position\" ); DriverPositionWindow positionWindow = new DriverPositionWindow ( driverZone . ZoneImage , new Rectangle ( posEl . GetProperty ( \"x\" ). GetInt32 (), posEl . GetProperty ( \"y\" ). GetInt32 (), posEl . GetProperty ( \"width\" ). GetInt32 (), posEl . GetProperty ( \"height\" ). GetInt32 ()), LoadOCR ); //GapToLeader JsonElement gapEl = windowElement . GetProperty ( \"GapToLeader\" ); DriverGapToLeaderWindow gapWindow = new DriverGapToLeaderWindow ( driverZone . ZoneImage , new Rectangle ( gapEl . GetProperty ( \"x\" ). GetInt32 (), gapEl . GetProperty ( \"y\" ). GetInt32 (), gapEl . GetProperty ( \"width\" ). GetInt32 (), gapEl . GetProperty ( \"height\" ). GetInt32 ()), LoadOCR ); //LapTime JsonElement lapEl = windowElement . GetProperty ( \"LapTime\" ); DriverLapTimeWindow lapWindow = new DriverLapTimeWindow ( driverZone . ZoneImage , new Rectangle ( lapEl . GetProperty ( \"x\" ). GetInt32 (), lapEl . GetProperty ( \"y\" ). GetInt32 (), lapEl . GetProperty ( \"width\" ). GetInt32 (), lapEl . GetProperty ( \"height\" ). GetInt32 ()), LoadOCR ); //DRS JsonElement drsEl = windowElement . GetProperty ( \"DRS\" ); DriverDrsWindow drsWindow = new DriverDrsWindow ( driverZone . ZoneImage , new Rectangle ( drsEl . GetProperty ( \"x\" ). GetInt32 (), drsEl . GetProperty ( \"y\" ). GetInt32 (), drsEl . GetProperty ( \"width\" ). GetInt32 (), drsEl . GetProperty ( \"height\" ). GetInt32 ()), LoadOCR ); //Tyre JsonElement tyresEl = windowElement . GetProperty ( \"Tyres\" ); DriverTyresWindow tyreWindow = new DriverTyresWindow ( driverZone . ZoneImage , new Rectangle ( tyresEl . GetProperty ( \"x\" ). GetInt32 (), tyresEl . GetProperty ( \"y\" ). GetInt32 (), tyresEl . GetProperty ( \"width\" ). GetInt32 (), tyresEl . GetProperty ( \"height\" ). GetInt32 ()), LoadOCR ); //Name JsonElement nameEl = windowElement . GetProperty ( \"Name\" ); DriverNameWindow nameWindow = new DriverNameWindow ( driverZone . ZoneImage , new Rectangle ( nameEl . GetProperty ( \"x\" ). GetInt32 (), nameEl . GetProperty ( \"y\" ). GetInt32 (), nameEl . GetProperty ( \"width\" ). GetInt32 (), nameEl . GetProperty ( \"height\" ). GetInt32 ()), LoadOCR ); //Sector1 JsonElement sec1El = windowElement . GetProperty ( \"Sector1\" ); DriverSectorWindow sec1Window = new DriverSectorWindow ( driverZone . ZoneImage , new Rectangle ( sec1El . GetProperty ( \"x\" ). GetInt32 (), sec1El . GetProperty ( \"y\" ). GetInt32 (), sec1El . GetProperty ( \"width\" ). GetInt32 (), sec1El . GetProperty ( \"height\" ). GetInt32 ()), 1 , LoadOCR ); //Sector2 JsonElement sec2El = windowElement . GetProperty ( \"Sector2\" ); DriverSectorWindow sec2Window = new DriverSectorWindow ( driverZone . ZoneImage , new Rectangle ( sec2El . GetProperty ( \"x\" ). GetInt32 (), sec2El . GetProperty ( \"y\" ). GetInt32 (), sec2El . GetProperty ( \"width\" ). GetInt32 (), sec2El . GetProperty ( \"height\" ). GetInt32 ()), 2 , LoadOCR ); //Sector3 JsonElement sec3El = windowElement . GetProperty ( \"Sector3\" ); DriverSectorWindow sec3Window = new DriverSectorWindow ( driverZone . ZoneImage , new Rectangle ( sec3El . GetProperty ( \"x\" ). GetInt32 (), sec3El . GetProperty ( \"y\" ). GetInt32 (), sec3El . GetProperty ( \"width\" ). GetInt32 (), sec3El . GetProperty ( \"height\" ). GetInt32 ()), 3 , LoadOCR ); driverZone . AddWindow ( positionWindow ); driverZone . AddWindow ( gapWindow ); driverZone . AddWindow ( lapWindow ); driverZone . AddWindow ( drsWindow ); driverZone . AddWindow ( tyreWindow ); driverZone . AddWindow ( nameWindow ); driverZone . AddWindow ( sec1Window ); driverZone . AddWindow ( sec2Window ); driverZone . AddWindow ( sec3Window ); } mainZone . AddZone ( driverZone ); } JsonElement driversElement = main . GetProperty ( \"Drivers\" ); foreach ( JsonElement driverElement in driversElement . EnumerateArray ()) { string driverName = driverElement . GetString (); driverListToFill . Add ( driverName ); Storage . AddDriver ( driverName ); } mainZones . Add ( mainZone ); } catch ( IOException ex ) { MessageBox . Show ( \"Error reading JSON file: \" + ex . Message ); } catch ( JsonException ex ) { MessageBox . Show ( \"Invalid JSON format: \" + ex . Message ); } int driverID = 0 ; foreach ( Zone z in mainZones [ 0 ]. Zones ) { driverID ++; z . ZoneImage . Save ( \"LoadedDriver\" + driverID + \".png\" ); } 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 List < DriverData > Decode ( List < Zone > mainZones , List < string > drivers ) { List < DriverData > mainResults = new List < DriverData >(); //Decode for ( int mainZoneId = 0 ; mainZoneId < mainZones . Count ; mainZoneId ++) { switch ( mainZoneId ) { case 0 : //object lockObject = new object(); //Main Zone Parallel . For ( 0 , mainZones [ mainZoneId ]. Zones . Count , async i => //for (int i = 0; i < mainZones[mainZoneId].Zones.Count; i++) { DriverData data = mainZones [ mainZoneId ]. Zones [ i ]. Decode ( new List < string >( drivers )); mainResults . Add ( data ); DriverDataLogs [ i ]. Add ( data ); if ( data . Position != - 1 && DriverDataLogs [ i ]. Count > 1 ) { //Tries to fix the tyres if ( data . CurrentTyre . NumberOfLaps > DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. CurrentTyre . NumberOfLaps + 3 ) data . CurrentTyre . NumberOfLaps = DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. CurrentTyre . NumberOfLaps + 1 ; //Checking if its a new lap //If the third sector is filled but it was'nt the last time, then it means that a new Lap has been started //Lap detection can be f***ed if the OCR takes so much time that an entire sector can be raced without us knowing. if ( DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 1 ]. Sector3 != 0 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. Sector3 == 0 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. Position != - 1 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 1 ]. Position != - 1 ) { DriverData stats = new DriverData (); stats = DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 1 ]; DriverLaps [ i ]++; Storage . AddDriverStat ( stats , DriverLaps [ i ]); } //Checking if its a pitstop //Forget this the best way to know if a tyre has been changed is if the number of laps is zero if ( data . CurrentTyre . Coumpound != Tyre . Type . Undefined && data . CurrentTyre . NumberOfLaps == 0 && DriverDataLogs [ i ][ DriverDataLogs [ i ]. Count - 2 ]. CurrentTyre . NumberOfLaps != 0 ) { Storage . AddPitstop ( data . Name , DriverLaps [ i ] - 1 , data . CurrentTyre . Coumpound . ToString ()); //Driver laps -1 because it would take AT LEAST one lap for this program to detect a pitstop } } DriverDataLogs [ i ]. Add ( data ); }); break ; //Next there could be a Title Zone and TrackInfoZone } } //mainResults = mainResults.OrderBy(driver => driver.Position >= 0).ThenBy(driver => driver.Position).ToList(); mainResults = mainResults . OrderBy ( driver => driver . Position ). ToList (); return mainResults ; } /// <summary> /// Changes the image in all of the zones wich then will do the same for theyre own subzones and windows /// </summary> /// <param name=\"Image\">The new Image from the F1TV data channel</param> public void ChangeImage ( Bitmap Image ) { foreach ( Zone z in MainZones ) { z . Image = Image ; } } /// <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/Settings.html","text":"Settings.cs /// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : Settings.cs /// Brief : Class that controls the settings view /// Version : Beta 1.0 using 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 ; using TrackTrends ; namespace TrackTrends { public partial class Settings : Form { private string _grandPrixUrl = \"\" ; private string _selectedConfigFile ; 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 List < string > DriverList { get => _driverList ; private set => _driverList = value ; } public string SelectedConfigFile { get => _selectedConfigFile ; private set => _selectedConfigFile = value ; } //For the responsive content Size oldSize = new Size (); Size oldGpbxPreviewSize = new Size (); Size oldGpbxWindowPreviewSize = new Size (); Size oldPbxPreviewSize = new Size (); Size oldPbxWindowPreviewSize = new Size (); public Settings () { InitializeComponent (); Load (); } /// <summary> /// This methods regroups all the actions that the forms need to be doing at the first launch /// </summary> private void Load () { RefreshUI (); oldSize = this . Size ; oldGpbxPreviewSize = gpbxPreview . Size ; oldGpbxWindowPreviewSize = gpbxWindowPreview . Size ; oldPbxPreviewSize = pbxPreview . Size ; oldPbxWindowPreviewSize = pbxWindowPreview . Size ; btnLoadPreset . Enabled = false ; btnDeletePreset . Enabled = false ; btnSavePreset . Enabled = false ; // I prefered regrouping all the tooltips here to make it easier to edit (there is 100% of thoses sentences containing typos so if you see one dont hesitate to edit those messages) tip1 . SetToolTip ( btnCreatZone , \"After clicking you can select two points in the image to set the bounds of the important data\" ); tip1 . SetToolTip ( btnCreateWindow , \"After clicking this you will have to select all the windows that are important on the lower image. Refer to the documentation for more infos\" ); tip1 . SetToolTip ( btnRefresh , \"Starts the emulator or refreshes the images if its already running\" ); tip1 . SetToolTip ( btnResetDriver , \"Resets the driver if something went wrong or if you want to test an other URL\" ); tip1 . SetToolTip ( lsbDrivers , \"The drivers that are on the image. Non-Case sensitive\" ); tip1 . SetToolTip ( tbxPresetName , \"The name of the preset you want to save\" ); tip1 . SetToolTip ( pbxPreview , \"What the emulator returns\" ); tip1 . SetToolTip ( pbxWindowPreview , \"One of the driver zones that the program managed to slice from the main zone\" ); } /// <summary> /// This is the main method that will be called anytime something changes on the view /// It can be called at any time and will adapt the UI taking into account the state of the app /// </summary> 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 ) { pbxPreview . Image = Config . MainZone . Draw (); if ( Config . MainZone . Zones . Count > 0 ) pbxWindowPreview . Image = Config . MainZone . Zones [ 0 ]. Draw (); } } /// <summary> /// This will create a new zone but will require two points (one at each opposing sides and corners) /// </summary> /// <param name=\"p1\">The first corner (usually top left)</param> /// <param name=\"p2\">The second corner (usually bottom right)</param> private void CreateNewZone ( Point p1 , Point p2 ) { Rectangle dimensions = CreateAbsoluteRectangle ( p1 , p2 ); Config = new ConfigurationTool (( Bitmap ) pbxPreview . Image , dimensions ); RefreshUI (); } /// <summary> /// Creates all the windows with an array of rectangles /// </summary> /// <param name=\"dimensions\">An array that contains all the windows bounds and position (expects 9)</param> private void CreateWindows ( List < Rectangle > dimensions ) { if ( Config != null ) { Config . AddWindows ( dimensions ); } } /// <summary> /// Will just change the main URL /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void tbxGpUrl_TextChanged ( object sender , EventArgs e ) { GrandPrixUrl = tbxGpUrl . Text ; } /// <summary> /// Adds a driver into the driver list /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnAddDriver_Click ( object sender , EventArgs e ) { string newDriver = tbxDriverName . Text ; DriverList . Add ( newDriver ); tbxDriverName . Text = \"\" ; RefreshUI (); } /// <summary> /// Removes a driver from the drivers list /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnRemoveDriver_Click ( object sender , EventArgs e ) { if ( lsbDrivers . SelectedIndex >= 0 ) { DriverList . RemoveAt ( lsbDrivers . SelectedIndex ); } RefreshUI (); } /// <summary> /// Will change everything that needs to be changed for when the users starts or stops creating a zone /// </summary> 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 ; pbxPreview . Image = Emulator . Screenshot (); } ZoneP1 = new Point (- 1 , - 1 ); ZoneP2 = new Point (- 1 , - 1 ); lblZonePointsRemaning . Text = \"2 Points left\" ; } RefreshUI (); } /// <summary> /// Will change everything that needs to be changed for when the users starts or stops creating a window /// </summary> 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 (); } /// <summary> /// If the user is supposed to create a zone, will record the position of the clicks /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void pbxMain_MouseClick ( object sender , MouseEventArgs e ) { if ( CreatingZone && pbxPreview . Image != null ) { //Point coordinates = pbxMain.PointToClient(new Point(MousePosition.X, MousePosition.Y)); Point coordinates = e . Location ; float xOffset = ( float ) pbxPreview . Image . Width / ( float ) pbxPreview . Width ; float yOffset = ( float ) pbxPreview . Image . Height / ( float ) pbxPreview . 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 (); } } /// <summary> /// If the user is supposed to create a window, will record the position of the clicks /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void pbxDriverZone_MouseClick ( object sender , MouseEventArgs e ) { if ( CreatingWindow && pbxWindowPreview . Image != null ) { Point coordinates = e . Location ; float xOffset = ( float ) pbxWindowPreview . Image . Width / ( float ) pbxWindowPreview . Width ; float yOffset = ( float ) pbxWindowPreview . Image . Height / ( float ) pbxWindowPreview . 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 , pbxWindowPreview . Image . Height ); CreateWindows ( WindowsToAdd ); SwitchWindowCreation (); } } RefreshUI (); } } /// <summary> /// Creates a rectangle without caring about the order of the points. /// </summary> /// <param name=\"p1\">First point. Can be top left or bottom right</param> /// <param name=\"p2\">Second point. Can be top left or bottom right</param> /// <returns></returns> private Rectangle CreateAbsoluteRectangle ( Point p1 , Point p2 ) { Point newP1 = new Point (); Point newP2 = new Point (); //Kind of a pain to have to do this but this lets the user do stupid things without the app crashing 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 ); } /// <summary> /// Will refresh the emulator and will controll some of the controls /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private async void btnRefresh_Click ( object sender , EventArgs e ) { btnRefresh . Enabled = false ; btnCreatZone . Enabled = false ; btnCreateWindow . Enabled = false ; btnResetDriver . 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 100 : message = \"Error \" + errorCode + \" Could not recover cookies. It could be because of an improper installation of python or bad cookies in the chrome database. Please try to log on to the F1TV using chrome again\" ; break ; 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 ); btnRefresh . Text = \"Retry\" ; btnLoadPreset . Enabled = false ; btnDeletePreset . Enabled = false ; btnSavePreset . Enabled = false ; btnCreatZone . Enabled = false ; btnCreateWindow . Enabled = false ; btnResetDriver . Enabled = false ; } else { btnRefresh . Text = \"Get a newer image\" ; pbxPreview . Image = Emulator . Screenshot (); btnLoadPreset . Enabled = true ; btnDeletePreset . Enabled = true ; btnSavePreset . Enabled = true ; btnCreatZone . Enabled = true ; btnCreateWindow . Enabled = true ; btnResetDriver . Enabled = true ; } } else { pbxPreview . Image = Emulator . Screenshot (); //I know im repeating myself. This part could use a bool variable that allows those buttons to be displayed but it was the fastest way to fix a bad behaviour in the app btnLoadPreset . Enabled = true ; btnDeletePreset . Enabled = true ; btnSavePreset . Enabled = true ; btnCreatZone . Enabled = true ; btnCreateWindow . Enabled = true ; btnResetDriver . Enabled = true ; } btnRefresh . Enabled = true ; } /// <summary> /// Will try to close the headless browser so the main form can launch a new one safely /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void Settings_FormClosing ( object sender , FormClosingEventArgs e ) { if ( Emulator != null ) { Emulator . Stop (); } Emulator = null ; GC . Collect (); } /// <summary> /// Will reset the drivers /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnResetDriver_Click ( object sender , EventArgs e ) { if ( Emulator != null ) { Emulator . ResetDriver (); } } /// <summary> /// Saves the current presets as a new JSON file /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnSavePreset_Click ( object sender , EventArgs e ) { string presetName = tbxPresetName . Text ; if ( Config != null ) { Config . SaveToJson ( DriverList , presetName ); } RefreshUI (); } /// <summary> /// Will change the selected preset. Usefull if you close this page because then the main form will keep in memory your last choice /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void lsbPresets_SelectedIndexChanged ( object sender , EventArgs e ) { if ( lsbPresets . SelectedIndex >= 0 ) SelectedConfigFile = ( string ) lsbPresets . Items [ lsbPresets . SelectedIndex ]; } /// <summary> /// Will load an existing presets /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnLoadPreset_Click ( object sender , EventArgs e ) { //MessageBox.Show(lsbPresets.SelectedIndex.ToString()); if ( lsbPresets . SelectedIndex >= 0 && pbxPreview . Image != null ) { try { string fileName = lsbPresets . Items [ lsbPresets . SelectedIndex ]. ToString (); Reader reader = new Reader ( fileName , ( Bitmap ) pbxPreview . Image , false ); //MainZones #0 is the big main zone containing driver zones Config = new ConfigurationTool (( Bitmap ) pbxPreview . Image , reader . MainZones [ 0 ]. Bounds ); Config . MainZone = reader . MainZones [ 0 ]; DriverList = reader . Drivers ; SelectedConfigFile = fileName ; } catch ( Exception ex ) { MessageBox . Show ( \"Could not load the settings error :\" + ex ); } RefreshUI (); } } /// <summary> /// This will be called everytime the form resizes. Here we are making the form responsive /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void Settings_Resize ( object sender , EventArgs e ) { int xDiff = this . Width - oldSize . Width ; int yDiff = this . Height - oldSize . Height ; gpbxPreview . Size = new Size ( oldGpbxPreviewSize . Width + xDiff , oldGpbxPreviewSize . Height + yDiff ); gpbxWindowPreview . Size = new Size ( oldGpbxWindowPreviewSize . Width + xDiff , oldGpbxWindowPreviewSize . Height ); pbxPreview . Size = new Size ( oldPbxPreviewSize . Width + xDiff , oldPbxPreviewSize . Height + yDiff ); pbxWindowPreview . Size = new Size ( oldPbxWindowPreviewSize . Width + xDiff , oldPbxWindowPreviewSize . Height ); } /// <summary> /// Will delete an existing preset /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnDeletePreset_Click ( object sender , EventArgs e ) { int selectedIndex = lsbPresets . SelectedIndex ; if ( selectedIndex >= 0 ) { string fileName = lsbPresets . Items [ selectedIndex ]. ToString (); if ( File . Exists ( fileName )) { File . Delete ( fileName ); RefreshUI (); } else { MessageBox . Show ( \"Could not delete the preset because it does not exists\" ); } } } /// <summary> /// Sketchy method that is used to remove the borders from groupboxes... Yes its dumb but I dont think there is any other way /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void removeBorders ( object sender , PaintEventArgs e ) { GroupBox gpbx = ( GroupBox ) sender ; using ( Pen pen = new Pen ( gpbx . BackColor , 50 )) { e . Graphics . DrawRectangle ( pen , 0 , 0 , gpbx . Width - 1 , gpbx . Height - 1 ); e . Graphics . DrawRectangle ( pen , 0 , 0 , gpbx . Width - 1 , gpbx . Height - 1 ); } using ( var brush = new SolidBrush ( gpbx . ForeColor )) { var textPosition = new Point ( 5 , 0 ); // Adjust the X and Y values as needed e . Graphics . DrawString ( gpbx . Text , gpbx . Font , brush , textPosition ); } } } }","title":"Settings.cs"},{"location":"Code/Settings.html#settingscs","text":"/// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : Settings.cs /// Brief : Class that controls the settings view /// Version : Beta 1.0 using 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 ; using TrackTrends ; namespace TrackTrends { public partial class Settings : Form { private string _grandPrixUrl = \"\" ; private string _selectedConfigFile ; 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 List < string > DriverList { get => _driverList ; private set => _driverList = value ; } public string SelectedConfigFile { get => _selectedConfigFile ; private set => _selectedConfigFile = value ; } //For the responsive content Size oldSize = new Size (); Size oldGpbxPreviewSize = new Size (); Size oldGpbxWindowPreviewSize = new Size (); Size oldPbxPreviewSize = new Size (); Size oldPbxWindowPreviewSize = new Size (); public Settings () { InitializeComponent (); Load (); } /// <summary> /// This methods regroups all the actions that the forms need to be doing at the first launch /// </summary> private void Load () { RefreshUI (); oldSize = this . Size ; oldGpbxPreviewSize = gpbxPreview . Size ; oldGpbxWindowPreviewSize = gpbxWindowPreview . Size ; oldPbxPreviewSize = pbxPreview . Size ; oldPbxWindowPreviewSize = pbxWindowPreview . Size ; btnLoadPreset . Enabled = false ; btnDeletePreset . Enabled = false ; btnSavePreset . Enabled = false ; // I prefered regrouping all the tooltips here to make it easier to edit (there is 100% of thoses sentences containing typos so if you see one dont hesitate to edit those messages) tip1 . SetToolTip ( btnCreatZone , \"After clicking you can select two points in the image to set the bounds of the important data\" ); tip1 . SetToolTip ( btnCreateWindow , \"After clicking this you will have to select all the windows that are important on the lower image. Refer to the documentation for more infos\" ); tip1 . SetToolTip ( btnRefresh , \"Starts the emulator or refreshes the images if its already running\" ); tip1 . SetToolTip ( btnResetDriver , \"Resets the driver if something went wrong or if you want to test an other URL\" ); tip1 . SetToolTip ( lsbDrivers , \"The drivers that are on the image. Non-Case sensitive\" ); tip1 . SetToolTip ( tbxPresetName , \"The name of the preset you want to save\" ); tip1 . SetToolTip ( pbxPreview , \"What the emulator returns\" ); tip1 . SetToolTip ( pbxWindowPreview , \"One of the driver zones that the program managed to slice from the main zone\" ); } /// <summary> /// This is the main method that will be called anytime something changes on the view /// It can be called at any time and will adapt the UI taking into account the state of the app /// </summary> 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 ) { pbxPreview . Image = Config . MainZone . Draw (); if ( Config . MainZone . Zones . Count > 0 ) pbxWindowPreview . Image = Config . MainZone . Zones [ 0 ]. Draw (); } } /// <summary> /// This will create a new zone but will require two points (one at each opposing sides and corners) /// </summary> /// <param name=\"p1\">The first corner (usually top left)</param> /// <param name=\"p2\">The second corner (usually bottom right)</param> private void CreateNewZone ( Point p1 , Point p2 ) { Rectangle dimensions = CreateAbsoluteRectangle ( p1 , p2 ); Config = new ConfigurationTool (( Bitmap ) pbxPreview . Image , dimensions ); RefreshUI (); } /// <summary> /// Creates all the windows with an array of rectangles /// </summary> /// <param name=\"dimensions\">An array that contains all the windows bounds and position (expects 9)</param> private void CreateWindows ( List < Rectangle > dimensions ) { if ( Config != null ) { Config . AddWindows ( dimensions ); } } /// <summary> /// Will just change the main URL /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void tbxGpUrl_TextChanged ( object sender , EventArgs e ) { GrandPrixUrl = tbxGpUrl . Text ; } /// <summary> /// Adds a driver into the driver list /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnAddDriver_Click ( object sender , EventArgs e ) { string newDriver = tbxDriverName . Text ; DriverList . Add ( newDriver ); tbxDriverName . Text = \"\" ; RefreshUI (); } /// <summary> /// Removes a driver from the drivers list /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnRemoveDriver_Click ( object sender , EventArgs e ) { if ( lsbDrivers . SelectedIndex >= 0 ) { DriverList . RemoveAt ( lsbDrivers . SelectedIndex ); } RefreshUI (); } /// <summary> /// Will change everything that needs to be changed for when the users starts or stops creating a zone /// </summary> 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 ; pbxPreview . Image = Emulator . Screenshot (); } ZoneP1 = new Point (- 1 , - 1 ); ZoneP2 = new Point (- 1 , - 1 ); lblZonePointsRemaning . Text = \"2 Points left\" ; } RefreshUI (); } /// <summary> /// Will change everything that needs to be changed for when the users starts or stops creating a window /// </summary> 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 (); } /// <summary> /// If the user is supposed to create a zone, will record the position of the clicks /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void pbxMain_MouseClick ( object sender , MouseEventArgs e ) { if ( CreatingZone && pbxPreview . Image != null ) { //Point coordinates = pbxMain.PointToClient(new Point(MousePosition.X, MousePosition.Y)); Point coordinates = e . Location ; float xOffset = ( float ) pbxPreview . Image . Width / ( float ) pbxPreview . Width ; float yOffset = ( float ) pbxPreview . Image . Height / ( float ) pbxPreview . 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 (); } } /// <summary> /// If the user is supposed to create a window, will record the position of the clicks /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void pbxDriverZone_MouseClick ( object sender , MouseEventArgs e ) { if ( CreatingWindow && pbxWindowPreview . Image != null ) { Point coordinates = e . Location ; float xOffset = ( float ) pbxWindowPreview . Image . Width / ( float ) pbxWindowPreview . Width ; float yOffset = ( float ) pbxWindowPreview . Image . Height / ( float ) pbxWindowPreview . 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 , pbxWindowPreview . Image . Height ); CreateWindows ( WindowsToAdd ); SwitchWindowCreation (); } } RefreshUI (); } } /// <summary> /// Creates a rectangle without caring about the order of the points. /// </summary> /// <param name=\"p1\">First point. Can be top left or bottom right</param> /// <param name=\"p2\">Second point. Can be top left or bottom right</param> /// <returns></returns> private Rectangle CreateAbsoluteRectangle ( Point p1 , Point p2 ) { Point newP1 = new Point (); Point newP2 = new Point (); //Kind of a pain to have to do this but this lets the user do stupid things without the app crashing 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 ); } /// <summary> /// Will refresh the emulator and will controll some of the controls /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private async void btnRefresh_Click ( object sender , EventArgs e ) { btnRefresh . Enabled = false ; btnCreatZone . Enabled = false ; btnCreateWindow . Enabled = false ; btnResetDriver . 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 100 : message = \"Error \" + errorCode + \" Could not recover cookies. It could be because of an improper installation of python or bad cookies in the chrome database. Please try to log on to the F1TV using chrome again\" ; break ; 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 ); btnRefresh . Text = \"Retry\" ; btnLoadPreset . Enabled = false ; btnDeletePreset . Enabled = false ; btnSavePreset . Enabled = false ; btnCreatZone . Enabled = false ; btnCreateWindow . Enabled = false ; btnResetDriver . Enabled = false ; } else { btnRefresh . Text = \"Get a newer image\" ; pbxPreview . Image = Emulator . Screenshot (); btnLoadPreset . Enabled = true ; btnDeletePreset . Enabled = true ; btnSavePreset . Enabled = true ; btnCreatZone . Enabled = true ; btnCreateWindow . Enabled = true ; btnResetDriver . Enabled = true ; } } else { pbxPreview . Image = Emulator . Screenshot (); //I know im repeating myself. This part could use a bool variable that allows those buttons to be displayed but it was the fastest way to fix a bad behaviour in the app btnLoadPreset . Enabled = true ; btnDeletePreset . Enabled = true ; btnSavePreset . Enabled = true ; btnCreatZone . Enabled = true ; btnCreateWindow . Enabled = true ; btnResetDriver . Enabled = true ; } btnRefresh . Enabled = true ; } /// <summary> /// Will try to close the headless browser so the main form can launch a new one safely /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void Settings_FormClosing ( object sender , FormClosingEventArgs e ) { if ( Emulator != null ) { Emulator . Stop (); } Emulator = null ; GC . Collect (); } /// <summary> /// Will reset the drivers /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnResetDriver_Click ( object sender , EventArgs e ) { if ( Emulator != null ) { Emulator . ResetDriver (); } } /// <summary> /// Saves the current presets as a new JSON file /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnSavePreset_Click ( object sender , EventArgs e ) { string presetName = tbxPresetName . Text ; if ( Config != null ) { Config . SaveToJson ( DriverList , presetName ); } RefreshUI (); } /// <summary> /// Will change the selected preset. Usefull if you close this page because then the main form will keep in memory your last choice /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void lsbPresets_SelectedIndexChanged ( object sender , EventArgs e ) { if ( lsbPresets . SelectedIndex >= 0 ) SelectedConfigFile = ( string ) lsbPresets . Items [ lsbPresets . SelectedIndex ]; } /// <summary> /// Will load an existing presets /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnLoadPreset_Click ( object sender , EventArgs e ) { //MessageBox.Show(lsbPresets.SelectedIndex.ToString()); if ( lsbPresets . SelectedIndex >= 0 && pbxPreview . Image != null ) { try { string fileName = lsbPresets . Items [ lsbPresets . SelectedIndex ]. ToString (); Reader reader = new Reader ( fileName , ( Bitmap ) pbxPreview . Image , false ); //MainZones #0 is the big main zone containing driver zones Config = new ConfigurationTool (( Bitmap ) pbxPreview . Image , reader . MainZones [ 0 ]. Bounds ); Config . MainZone = reader . MainZones [ 0 ]; DriverList = reader . Drivers ; SelectedConfigFile = fileName ; } catch ( Exception ex ) { MessageBox . Show ( \"Could not load the settings error :\" + ex ); } RefreshUI (); } } /// <summary> /// This will be called everytime the form resizes. Here we are making the form responsive /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void Settings_Resize ( object sender , EventArgs e ) { int xDiff = this . Width - oldSize . Width ; int yDiff = this . Height - oldSize . Height ; gpbxPreview . Size = new Size ( oldGpbxPreviewSize . Width + xDiff , oldGpbxPreviewSize . Height + yDiff ); gpbxWindowPreview . Size = new Size ( oldGpbxWindowPreviewSize . Width + xDiff , oldGpbxWindowPreviewSize . Height ); pbxPreview . Size = new Size ( oldPbxPreviewSize . Width + xDiff , oldPbxPreviewSize . Height + yDiff ); pbxWindowPreview . Size = new Size ( oldPbxWindowPreviewSize . Width + xDiff , oldPbxWindowPreviewSize . Height ); } /// <summary> /// Will delete an existing preset /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void btnDeletePreset_Click ( object sender , EventArgs e ) { int selectedIndex = lsbPresets . SelectedIndex ; if ( selectedIndex >= 0 ) { string fileName = lsbPresets . Items [ selectedIndex ]. ToString (); if ( File . Exists ( fileName )) { File . Delete ( fileName ); RefreshUI (); } else { MessageBox . Show ( \"Could not delete the preset because it does not exists\" ); } } } /// <summary> /// Sketchy method that is used to remove the borders from groupboxes... Yes its dumb but I dont think there is any other way /// </summary> /// <param name=\"sender\"></param> /// <param name=\"e\"></param> private void removeBorders ( object sender , PaintEventArgs e ) { GroupBox gpbx = ( GroupBox ) sender ; using ( Pen pen = new Pen ( gpbx . BackColor , 50 )) { e . Graphics . DrawRectangle ( pen , 0 , 0 , gpbx . Width - 1 , gpbx . Height - 1 ); e . Graphics . DrawRectangle ( pen , 0 , 0 , gpbx . Width - 1 , gpbx . Height - 1 ); } using ( var brush = new SolidBrush ( gpbx . ForeColor )) { var textPosition = new Point ( 5 , 0 ); // Adjust the X and Y values as needed e . Graphics . DrawString ( gpbx . Text , gpbx . Font , brush , textPosition ); } } } }","title":"Settings.cs"},{"location":"Code/Window.html","text":"Window.cs /// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : Window.cs /// Brief : Default Window object that is mainly expected to be inherited. /// Version : Beta 1.0 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 TrackTrends { public class Window { public const string STRING_DEBUG_FOLDER = \"./GetString\" ; public const string LAPTIME_DEBUG_FOLDER = \"./LapTime\" ; public const string GAPTOLEADER_DEBUG_FOLDER = \"./Gap\" ; public const string SECTOR1_DEBUG_FOLDER = \"./Sector1\" ; public const string SECTOR2_DEBUG_FOLDER = \"./Sector2\" ; public const string SECTOR3_DEBUG_FOLDER = \"./Sector3\" ; public const string DRS_DEBUG_FOLDER = \"./DRS\" ; public const string TYRE_DEBUG_FOLDER = \"./Tyre\" ; 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\" ); //Debug public static Random rnd = new Random (); 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 ; } } /// <summary> /// Creates a new Window /// </summary> /// <param name=\"image\">The image of the parent zone</param> /// <param name=\"bounds\">The position and size of the window</param> /// <param name=\"generateEngine\">Does the window need to generate a tesseract engine (takes time and ressources)</param> 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 ; } //DEBUG /* if (!Directory.Exists(STRING_DEBUG_FOLDER)) Directory.CreateDirectory(STRING_DEBUG_FOLDER); if (!Directory.Exists(LAPTIME_DEBUG_FOLDER)) Directory.CreateDirectory(LAPTIME_DEBUG_FOLDER); if (!Directory.Exists(GAPTOLEADER_DEBUG_FOLDER)) Directory.CreateDirectory(GAPTOLEADER_DEBUG_FOLDER); if (!Directory.Exists(SECTOR1_DEBUG_FOLDER)) Directory.CreateDirectory(SECTOR1_DEBUG_FOLDER); if (!Directory.Exists(SECTOR2_DEBUG_FOLDER)) Directory.CreateDirectory(SECTOR2_DEBUG_FOLDER); if (!Directory.Exists(SECTOR3_DEBUG_FOLDER)) Directory.CreateDirectory(SECTOR3_DEBUG_FOLDER); if (!Directory.Exists(DRS_DEBUG_FOLDER)) Directory.CreateDirectory(DRS_DEBUG_FOLDER); if (!Directory.Exists(TYRE_DEBUG_FOLDER)) Directory.CreateDirectory(TYRE_DEBUG_FOLDER); */ } /// <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 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 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 int GetTimeFromPng ( Bitmap image , 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 ; //Debug int salt = rnd . Next ( 0 , 999999 ); 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 ( image ). 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 ) == \"\" ); int minuts = 0 ; int seconds = 0 ; int miliseconds = 0 ; switch ( windowType ) { case OcrImage . WindowType . Sector : //Usually there is supposed to be only 2 parts. if ( rawNumbers . Count == 2 ) { //The perect case try { seconds = Convert . ToInt32 ( rawNumbers [ 0 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ]. ToString ()); } catch { Console . WriteLine ( \"Sector time convertion failed\" ); } } else { if ( rawNumbers . Count == 1 ) { //Here it is a little harder... Usually its because a '.' has been overlooked or interpreted as a number if ( rawNumbers [ 0 ]. Length == 6 ) { //The '.' has been understood as a number try { seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString () + rawNumbers [ 0 ][ 1 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 3 ]. ToString () + rawNumbers [ 0 ][ 4 ]. ToString () + rawNumbers [ 0 ][ 5 ]. ToString ()); } catch { Console . WriteLine ( \"Sector time convertion failed\" ); } } else { if ( rawNumbers [ 0 ]. Length == 5 ) { //The '.' has been overlooked try { seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString () + rawNumbers [ 0 ][ 1 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString () + rawNumbers [ 0 ][ 4 ]. ToString ()); } catch { Console . WriteLine ( \"Sector time convertion failed\" ); } } else { Console . WriteLine ( \"Sector time convertion failed\" ); } } } else { //The OCR detected more than 1 '.' wich is concerning because that means that something went really wrong Console . WriteLine ( \"Sector time convertion failed\" ); } } result = 0 ; result += seconds * 1000 ; result += miliseconds ; break ; case OcrImage . WindowType . LapTime : if ( rawNumbers . Count == 3 ) { //The normal way try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 1 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 2 ]. ToString ()); } catch { Console . WriteLine ( \"Lap time convertion failed\" ); } } else { if ( rawNumbers . Count == 2 ) { //Either the ':' or the '.' has been missinterpreted if ( rawNumbers [ 0 ]. Length > rawNumbers [ 1 ]. Length ) { //The ':' has been missinterpreted if ( rawNumbers [ 0 ]. Length == 3 ) { //It has been forgotten try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 1 ]. ToString () + rawNumbers [ 0 ][ 2 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ]); } catch { Console . WriteLine ( \"Lap time convertion failed\" ); } } else { if ( rawNumbers [ 0 ]. Length == 4 ) { //I has been translated into an other number try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ]); } catch { Console . WriteLine ( \"Lap time convertion failed\" ); } } else { //This could happen if the ':' has been missinterpreted with a lap time of over 9 minuts (HIGLY IMPROBABLE) Console . WriteLine ( \"Lap time convertion failed\" ); } } } else { //The '.' has been missinterpreted if ( rawNumbers [ 1 ]. Length == 5 ) { //It has been forgotten minuts = Convert . ToInt32 ( rawNumbers [ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 1 ][ 0 ]. ToString () + rawNumbers [ 1 ][ 1 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ][ 2 ]. ToString () + rawNumbers [ 1 ][ 3 ]. ToString () + rawNumbers [ 1 ][ 4 ]. ToString ()); } else { if ( rawNumbers [ 1 ]. Length == 6 ) { try { //It has been interpreted as a number minuts = Convert . ToInt32 ( rawNumbers [ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 1 ][ 0 ]. ToString () + rawNumbers [ 1 ][ 1 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ][ 3 ]. ToString () + rawNumbers [ 1 ][ 4 ]. ToString () + rawNumbers [ 1 ][ 5 ]. ToString ()); } catch { //It can happen and to be honest I dont know how to fix it } } else { Console . WriteLine ( \"Lap time convertion failed\" ); } } } } else { if ( rawNumbers . Count == 1 ) { //Both the '.' and the ':' have been missinterpreted if ( rawNumbers [ 0 ]. Length == 6 ) { //The just all have been forgotten try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 1 ]. ToString () + rawNumbers [ 0 ][ 2 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 3 ]. ToString () + rawNumbers [ 0 ][ 4 ]. ToString () + rawNumbers [ 0 ][ 5 ]. ToString ()); } catch { Console . WriteLine ( \"Lap time convertion failed\" ); } } else { if ( rawNumbers [ 0 ]. Length == 7 ) { //The '.' or ':' have been interpreted as a number (usually the ':') try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 4 ]. ToString () + rawNumbers [ 0 ][ 5 ]. ToString () + rawNumbers [ 0 ][ 6 ]. ToString ()); } catch { Console . WriteLine ( \"Lap time convertion failed\" ); } } else { if ( rawNumbers [ 0 ]. Length == 8 ) { //Both have been interpreted as a number try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 5 ]. ToString () + rawNumbers [ 0 ][ 6 ]. ToString () + rawNumbers [ 0 ][ 7 ]. ToString ()); } catch { Console . WriteLine ( \"Lap time convertion failed\" ); } } else { //I dont know what could have happened Console . WriteLine ( \"Lap time convertion failed\" ); } } } } else { //I dont know what could have happened Console . WriteLine ( \"Lap time convertion failed\" ); } } } result = 0 ; result += minuts * 60 * 1000 ; result += seconds * 1000 ; result += miliseconds ; break ; case OcrImage . WindowType . Gap : if ( rawNumbers . Count == 2 ) { // This should be the x.xxx or a missed x:xx.xxx if ( rawNumbers [ 0 ]. Length > 2 ) { //Its a missed x:xx.xxx if ( rawNumbers [ 0 ]. Length == 3 ) { //It forgot the \":\" try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 1 ]. ToString () + rawNumbers [ 0 ][ 2 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ]); } catch { Console . WriteLine ( \"Gap to leader convertion failed\" ); } } else { //The \":\" has been mistaken as a number if ( rawNumbers [ 0 ]. Length == 4 ) { try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ]); } catch { Console . WriteLine ( \"Gap to leader convertion failed\" ); } } else { Console . WriteLine ( \"Gap to leader convertion failed\" ); } } } else { //It should be a normal x.xxx or xx.xxx try { seconds = Convert . ToInt32 ( rawNumbers [ 0 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ]. ToString ()); } catch { Console . WriteLine ( \"Gap to leader convertion failed\" ); } } } else { if ( rawNumbers . Count == 1 ) { //can be anything depending on the size of the string if ( rawNumbers [ 0 ]. Length == 4 ) { //We just missed the '.' try { seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 1 ]. ToString () + rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString ()); } catch { Console . WriteLine ( \"Gap to leader convertion failed\" ); } } else { if ( rawNumbers [ 0 ]. Length == 5 ) { //We just missed the '.' try { seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString () + rawNumbers [ 0 ][ 1 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString () + rawNumbers [ 0 ][ 4 ]. ToString ()); } catch { Console . WriteLine ( \"Gap to leader convertion failed\" ); } } //There is just too much possibilities that it would be stupid to try and tell them appart so for now im leaving that as just an error Console . WriteLine ( \"Gap to leader convertion failed\" ); } } else { if ( rawNumbers . Count == 3 ) { // This should be the x:xx.xxx try { //Gaps cant be more than 9 minuts so if there is more than 1 digit it means that the '+' has been understood as an other number if ( rawNumbers [ 0 ]. Length > 1 ) rawNumbers [ 0 ] = rawNumbers [ 0 ][ rawNumbers [ 0 ]. Length - 1 ]. ToString (); minuts = Convert . ToInt32 ( rawNumbers [ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 1 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 2 ]. ToString ()); } catch { Console . WriteLine ( \"Gap to leader convertion failed\" ); } } } } result = 0 ; result += minuts * 60 * 1000 ; result += seconds * 1000 ; result += miliseconds ; break ; default : try { result = Convert . ToInt32 ( rawNumbers [ 0 ]. ToString ()); } catch { result = 0 ; } break ; } 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 string GetStringFromPng ( Bitmap image , TesseractEngine Engine , string allowedChars = \"\" , OcrImage . WindowType windowType = OcrImage . WindowType . Text ) { string result = \"\" ; //Debug int salt = rnd . Next ( 0 , 999999 ); Engine . SetVariable ( \"tessedit_char_whitelist\" , allowedChars ); Bitmap rawData = image ; 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 ]; } } }","title":"Window.cs"},{"location":"Code/Window.html#windowcs","text":"/// Author : Maxime Rohmer /// Date : 09/06/2023 /// File : Window.cs /// Brief : Default Window object that is mainly expected to be inherited. /// Version : Beta 1.0 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 TrackTrends { public class Window { public const string STRING_DEBUG_FOLDER = \"./GetString\" ; public const string LAPTIME_DEBUG_FOLDER = \"./LapTime\" ; public const string GAPTOLEADER_DEBUG_FOLDER = \"./Gap\" ; public const string SECTOR1_DEBUG_FOLDER = \"./Sector1\" ; public const string SECTOR2_DEBUG_FOLDER = \"./Sector2\" ; public const string SECTOR3_DEBUG_FOLDER = \"./Sector3\" ; public const string DRS_DEBUG_FOLDER = \"./DRS\" ; public const string TYRE_DEBUG_FOLDER = \"./Tyre\" ; 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\" ); //Debug public static Random rnd = new Random (); 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 ; } } /// <summary> /// Creates a new Window /// </summary> /// <param name=\"image\">The image of the parent zone</param> /// <param name=\"bounds\">The position and size of the window</param> /// <param name=\"generateEngine\">Does the window need to generate a tesseract engine (takes time and ressources)</param> 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 ; } //DEBUG /* if (!Directory.Exists(STRING_DEBUG_FOLDER)) Directory.CreateDirectory(STRING_DEBUG_FOLDER); if (!Directory.Exists(LAPTIME_DEBUG_FOLDER)) Directory.CreateDirectory(LAPTIME_DEBUG_FOLDER); if (!Directory.Exists(GAPTOLEADER_DEBUG_FOLDER)) Directory.CreateDirectory(GAPTOLEADER_DEBUG_FOLDER); if (!Directory.Exists(SECTOR1_DEBUG_FOLDER)) Directory.CreateDirectory(SECTOR1_DEBUG_FOLDER); if (!Directory.Exists(SECTOR2_DEBUG_FOLDER)) Directory.CreateDirectory(SECTOR2_DEBUG_FOLDER); if (!Directory.Exists(SECTOR3_DEBUG_FOLDER)) Directory.CreateDirectory(SECTOR3_DEBUG_FOLDER); if (!Directory.Exists(DRS_DEBUG_FOLDER)) Directory.CreateDirectory(DRS_DEBUG_FOLDER); if (!Directory.Exists(TYRE_DEBUG_FOLDER)) Directory.CreateDirectory(TYRE_DEBUG_FOLDER); */ } /// <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 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 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 int GetTimeFromPng ( Bitmap image , 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 ; //Debug int salt = rnd . Next ( 0 , 999999 ); 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 ( image ). 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 ) == \"\" ); int minuts = 0 ; int seconds = 0 ; int miliseconds = 0 ; switch ( windowType ) { case OcrImage . WindowType . Sector : //Usually there is supposed to be only 2 parts. if ( rawNumbers . Count == 2 ) { //The perect case try { seconds = Convert . ToInt32 ( rawNumbers [ 0 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ]. ToString ()); } catch { Console . WriteLine ( \"Sector time convertion failed\" ); } } else { if ( rawNumbers . Count == 1 ) { //Here it is a little harder... Usually its because a '.' has been overlooked or interpreted as a number if ( rawNumbers [ 0 ]. Length == 6 ) { //The '.' has been understood as a number try { seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString () + rawNumbers [ 0 ][ 1 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 3 ]. ToString () + rawNumbers [ 0 ][ 4 ]. ToString () + rawNumbers [ 0 ][ 5 ]. ToString ()); } catch { Console . WriteLine ( \"Sector time convertion failed\" ); } } else { if ( rawNumbers [ 0 ]. Length == 5 ) { //The '.' has been overlooked try { seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString () + rawNumbers [ 0 ][ 1 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString () + rawNumbers [ 0 ][ 4 ]. ToString ()); } catch { Console . WriteLine ( \"Sector time convertion failed\" ); } } else { Console . WriteLine ( \"Sector time convertion failed\" ); } } } else { //The OCR detected more than 1 '.' wich is concerning because that means that something went really wrong Console . WriteLine ( \"Sector time convertion failed\" ); } } result = 0 ; result += seconds * 1000 ; result += miliseconds ; break ; case OcrImage . WindowType . LapTime : if ( rawNumbers . Count == 3 ) { //The normal way try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 1 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 2 ]. ToString ()); } catch { Console . WriteLine ( \"Lap time convertion failed\" ); } } else { if ( rawNumbers . Count == 2 ) { //Either the ':' or the '.' has been missinterpreted if ( rawNumbers [ 0 ]. Length > rawNumbers [ 1 ]. Length ) { //The ':' has been missinterpreted if ( rawNumbers [ 0 ]. Length == 3 ) { //It has been forgotten try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 1 ]. ToString () + rawNumbers [ 0 ][ 2 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ]); } catch { Console . WriteLine ( \"Lap time convertion failed\" ); } } else { if ( rawNumbers [ 0 ]. Length == 4 ) { //I has been translated into an other number try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ]); } catch { Console . WriteLine ( \"Lap time convertion failed\" ); } } else { //This could happen if the ':' has been missinterpreted with a lap time of over 9 minuts (HIGLY IMPROBABLE) Console . WriteLine ( \"Lap time convertion failed\" ); } } } else { //The '.' has been missinterpreted if ( rawNumbers [ 1 ]. Length == 5 ) { //It has been forgotten minuts = Convert . ToInt32 ( rawNumbers [ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 1 ][ 0 ]. ToString () + rawNumbers [ 1 ][ 1 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ][ 2 ]. ToString () + rawNumbers [ 1 ][ 3 ]. ToString () + rawNumbers [ 1 ][ 4 ]. ToString ()); } else { if ( rawNumbers [ 1 ]. Length == 6 ) { try { //It has been interpreted as a number minuts = Convert . ToInt32 ( rawNumbers [ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 1 ][ 0 ]. ToString () + rawNumbers [ 1 ][ 1 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ][ 3 ]. ToString () + rawNumbers [ 1 ][ 4 ]. ToString () + rawNumbers [ 1 ][ 5 ]. ToString ()); } catch { //It can happen and to be honest I dont know how to fix it } } else { Console . WriteLine ( \"Lap time convertion failed\" ); } } } } else { if ( rawNumbers . Count == 1 ) { //Both the '.' and the ':' have been missinterpreted if ( rawNumbers [ 0 ]. Length == 6 ) { //The just all have been forgotten try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 1 ]. ToString () + rawNumbers [ 0 ][ 2 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 3 ]. ToString () + rawNumbers [ 0 ][ 4 ]. ToString () + rawNumbers [ 0 ][ 5 ]. ToString ()); } catch { Console . WriteLine ( \"Lap time convertion failed\" ); } } else { if ( rawNumbers [ 0 ]. Length == 7 ) { //The '.' or ':' have been interpreted as a number (usually the ':') try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 4 ]. ToString () + rawNumbers [ 0 ][ 5 ]. ToString () + rawNumbers [ 0 ][ 6 ]. ToString ()); } catch { Console . WriteLine ( \"Lap time convertion failed\" ); } } else { if ( rawNumbers [ 0 ]. Length == 8 ) { //Both have been interpreted as a number try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 5 ]. ToString () + rawNumbers [ 0 ][ 6 ]. ToString () + rawNumbers [ 0 ][ 7 ]. ToString ()); } catch { Console . WriteLine ( \"Lap time convertion failed\" ); } } else { //I dont know what could have happened Console . WriteLine ( \"Lap time convertion failed\" ); } } } } else { //I dont know what could have happened Console . WriteLine ( \"Lap time convertion failed\" ); } } } result = 0 ; result += minuts * 60 * 1000 ; result += seconds * 1000 ; result += miliseconds ; break ; case OcrImage . WindowType . Gap : if ( rawNumbers . Count == 2 ) { // This should be the x.xxx or a missed x:xx.xxx if ( rawNumbers [ 0 ]. Length > 2 ) { //Its a missed x:xx.xxx if ( rawNumbers [ 0 ]. Length == 3 ) { //It forgot the \":\" try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 1 ]. ToString () + rawNumbers [ 0 ][ 2 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ]); } catch { Console . WriteLine ( \"Gap to leader convertion failed\" ); } } else { //The \":\" has been mistaken as a number if ( rawNumbers [ 0 ]. Length == 4 ) { try { minuts = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ]); } catch { Console . WriteLine ( \"Gap to leader convertion failed\" ); } } else { Console . WriteLine ( \"Gap to leader convertion failed\" ); } } } else { //It should be a normal x.xxx or xx.xxx try { seconds = Convert . ToInt32 ( rawNumbers [ 0 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 1 ]. ToString ()); } catch { Console . WriteLine ( \"Gap to leader convertion failed\" ); } } } else { if ( rawNumbers . Count == 1 ) { //can be anything depending on the size of the string if ( rawNumbers [ 0 ]. Length == 4 ) { //We just missed the '.' try { seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 1 ]. ToString () + rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString ()); } catch { Console . WriteLine ( \"Gap to leader convertion failed\" ); } } else { if ( rawNumbers [ 0 ]. Length == 5 ) { //We just missed the '.' try { seconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 0 ]. ToString () + rawNumbers [ 0 ][ 1 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 0 ][ 2 ]. ToString () + rawNumbers [ 0 ][ 3 ]. ToString () + rawNumbers [ 0 ][ 4 ]. ToString ()); } catch { Console . WriteLine ( \"Gap to leader convertion failed\" ); } } //There is just too much possibilities that it would be stupid to try and tell them appart so for now im leaving that as just an error Console . WriteLine ( \"Gap to leader convertion failed\" ); } } else { if ( rawNumbers . Count == 3 ) { // This should be the x:xx.xxx try { //Gaps cant be more than 9 minuts so if there is more than 1 digit it means that the '+' has been understood as an other number if ( rawNumbers [ 0 ]. Length > 1 ) rawNumbers [ 0 ] = rawNumbers [ 0 ][ rawNumbers [ 0 ]. Length - 1 ]. ToString (); minuts = Convert . ToInt32 ( rawNumbers [ 0 ]. ToString ()); seconds = Convert . ToInt32 ( rawNumbers [ 1 ]. ToString ()); miliseconds = Convert . ToInt32 ( rawNumbers [ 2 ]. ToString ()); } catch { Console . WriteLine ( \"Gap to leader convertion failed\" ); } } } } result = 0 ; result += minuts * 60 * 1000 ; result += seconds * 1000 ; result += miliseconds ; break ; default : try { result = Convert . ToInt32 ( rawNumbers [ 0 ]. ToString ()); } catch { result = 0 ; } break ; } 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 string GetStringFromPng ( Bitmap image , TesseractEngine Engine , string allowedChars = \"\" , OcrImage . WindowType windowType = OcrImage . WindowType . Text ) { string result = \"\" ; //Debug int salt = rnd . Next ( 0 , 999999 ); Engine . SetVariable ( \"tessedit_char_whitelist\" , allowedChars ); Bitmap rawData = image ; 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 ]; } } }","title":"Window.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"}]}