189 KiB
Rapport Track Trends V1.0
Rohmer Maxime Travail de diplôme Technicien ES 2023
Introduction
Résumé
Track Trends est un outil de récupération et d'analyse de données de courses de Formule 1.
Pour le contexte, en dehors des cours, j'exerce différentes activités dont celle de Live Ticker F1 pour le 20 minutes. Pour m'aider dans ce travail, j'utilise actuellement la F1TV à laquelle je suis abonné qui me propose non seulement un feed vidéo de meilleure qualité avec des commentaires plus pertinents que ceux de la RTS mais qui aussi me permet d'accéder à un feed vidéo très important : la chaine data.
Ce dernier ressemble à cela :
(Attention, ce n'est pas un joli tableau HTML, mais bien une vidéo qui contient un tableau.)
Sauf que toutes les informations sont étalées pêle-mêle sans hiérarchie, ce qui fait que cela me prendrait trop de temps de tout déchiffrer à chaque fois, ce qui me fait rater des choses intéressantes.
Le but du projet est donc de fournir un outil qui hiérarchise et affiche différemment les données 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é dans le résumé, 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âches à effectuer :
- Suivre les différents évènements du Grand Prix
- Changer le titre et la photo de titre du Live
- Chercher des Tweets ou des Images à intégrer
- Ecrire les commentaires en faisant attention à dire ce qu'il se passe mais aussi l’expliquer, ce que cela implique, mais aussi ce que cela veut dire pour le reste de la course.
- Comprendre et expliquer les stratégies
Tout cela toutes les cinq minutes max...
Cela veut dire que je dois être le plus rapide possible quand je cherche des informations. Et comme le tableau en comporte trop et bien, je suis obligé de le lire en diagonale.
Par exemple dans le tableau, les infos que je cherche le plus sont :
- Le nombre de places gagnées (surtout au départ)
- Les écarts 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êts aux stands
- Les temps au tour (surtout pour la stratégie)
Mais pleins d'autres informations existent si on les recoupe sur plusieurs tours.
Un outil qui permettrait de mettre en évidence les informations importantes serait donc une très grosse plus-value pour mon travail et s'il est facile à installer et à utiliser, il se pourrait qu'il devienne indispensable.
Cahier des charges
Il s'agit d'une version coupée du cahier des charges qui ne contient pas l'explication du contexte. Mais l'original est disponible sur ce site également. Il est toutefois normal d'y voir des choses répétées ou légèrement différentes, en effet, il n'a pas été écrit en même temps que le reste de ce document.
Projet
Un outil de style compagnon sous forme d'application C# Windows Form qui récupère en temps réel 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éliorer 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éliorent leur temps au tour et ceux qui perdent le plus de temps
- Le classement pondéré tenant compte des futurs arrêts au stand
Maintenant afficher différemment les infos, c'est sympa, mais cela serait encore mieux de traiter ces data et de permettre des petites prédictions.
Exemples :
- Prédire les arrêts aux stands en prenant en compte les baisses de performances des pneus
- Prédire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour
- Prédire dans combien de tour tel pilote va rattraper tel autre pilote
- Prédire combien de temps le pilote va perdre dans les stands en fonctions des arrêts précédents
Réalisation
Malheureusement, la Formula 1 Management ne propose aucune API publique qui puisse nous permettre de faire ce projet "simplement". La raison la plus probable étant qu'Amazon avec son service AWS propose exactement ce genre de services pour le flux télévisé et il doit y avoir un contrat d'exclusivité.
Il existe des API "Pirates" faites par la communauté, le problème est qu'elles ne sont pas forcément des plus pratiques à utiliser.
Mais comme je possède un abonnement premium ++ à la F1TV, j'ai accès pour chaque grand prix à un flux vidéo nommé : DATA F1 CHANNEL
Qui ressemble à ça :
Donc la seule façon que je vois de récupérer ces données est de les prendre directement sur ce feed.
Même si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout être la récupération des données et leur stockage.
Les données viennent du flux vidéo et ainsi dans un premier temps, il va falloir récupérer d'une manière 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évue pour (exemple Tesseract) et vérifier l'intégrité de ces dernières pour qu'on puisse ensuite les stocker.
Dans un troisième temps, il faut stocker toutes ces données dans une forme qui permette d'aller facilement faire des requêtes de récupération et déjà préparer des méthodes qui permettent de récupérer des infos importantes (ex : la moyenne des cinq derniers tours, le temps moyen d'arrêt, etc.) pour faciliter la dernière étape
Quand tout cela est fait, on peut ensuite s'amuser un peu avec les Data.
La dernière étape est donc l'affichage. L'idée est de créer 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 à quoi l'application pourrait ressembler)
Voici la liste des données qui pourraient être affichées (Non contractuel, simplement des idées).
- Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre).
- Les pilotes qui améliorent leur temps au tour et ceux qui perdent le plus de temps
- Le classement pondéré tenant compte des futurs arrêts 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ême si c'est possible :
- Prédire les arrêts aux stands en prenant en compte les baisses de performances des pneus
- Prédire le pneu que le pilote va chausser s'il rentre aux stands dans le prochain tour
- Prédire dans combien de tour tel pilote va rattraper tel autre pilote
- Prédire combien de temps le pilote va perdre dans les stands en fonctions des arrêts précédents
- Prédire les temps au tour de chaque pilote selon l'usure des pneus
Voici un exemple d'interface possible pour une page :
Cas d'utilisation
'*'On va considérer que tous les user ont un abonnement F1 TV PRO
Un user veut récupérer 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ère utilisation).
- Il confirme que les données initiales sont bonnes (pour la première utilisation).
- Il regarde tranquille son Grand Prix
Le programme récupère les data :
- Il récupère des images depuis la F1TV
- Il utilise Tesseract (ou autre) pour en récupérer les infos.
- Il met ces infos dans un Objet Pilote, dans un Objet course avec un attribut tour pour hiérarchiser les data
Pour ce qui est de l'affichage, l'idée est de faire une application C# comme on l'a appris à l'école, mais avec assez de style pour qu'elle puisse être agréable à utiliser.
Quand le programme affiche les data :
- Il prend les données venant directement de la F1TV.
- Il affiche différemment les données pour permettre une meilleure lisibilité
- Il interprète avec des règles plutôt simples certaines data pour faire des miniprédictions ou aider à la lecture
- Il récupère des infos d'autres courses pour les comparer et proposer des prédictions plus intéressantes
Difficultés techniques
- Récupérer un flux vidéo plutôt propre malgré les contres mesures de la F1 TV pour en empêcher la lecture par un logiciel
- Si on doit passer par une capture d'écran, trouver un moyen de stocker les données de manière à prévoir que parfois un tour pourrait avoir plus de données qu'un autre, que le user peut mettre pause, ou même qu’il revienne en arrière.
- Développer des algorithmes pour récupérer les données comme les différents pneus utilisés ou l'activation du DRS ainsi que développer des moyens de nettoyer les résultats de l'OCR (Par exemple utiliser différents champs redondants pour comparer les résultats)
- Stocker les données sur une base pour les traiter plus tard tout en prévoyant un moyen de voir les stats live
- Développer des algorithmes de prédiction qui prennent en compte d'anciennes courses pour tenter de prédire des choses comme les arrêts aux stands par exemple.
Différences sur le cahier des charges
Ici, je vais parler de l'état du projet à la date du 12 Juin 2023.
À cette date, le projet est fonctionnel, mais comporte quelques différences avec le cahier des charges original. Je vais expliquer non seulement ces différences, mais aussi les raisons qui font qu'elles sont là.
Pour bien comprendre les différences, il faut s'en référer au cahier des charges original.
L'application doit être "Un outil de style compagnon sous forme d'application C# Windows Form qui récupère en temps réel les informations de la course et affiche les informations les plus importantes". C'est ça la phrase la plus importante dans tout le CDC. Et je pense que très honnêtement, ce cahier des charges est rempli !
L'application actuellement disponible sur le repos GIT est une application de style compagnion Windows Forms qui récupère les infos de la F1TV en temps réel et elle affiche les informations qu'elle trouve importante. Donc, je dirais que l'objectif général est rempli.
Maintenant, c'est dans les détails que cela pêche.
Il est mentionné trois exemples d'infos à 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éliorent leur temps au tour et ceux qui perdent le plus de temps"
- "Le classement pondéré tenant compte des futurs arrêts au stand"
résultats :
- Dans l'application, on peut effectivement voir les pilotes proches (Ce sont ceux qui sont à 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éré selon les arrêts aux stands, car l'application a du mal à détecter des arrêts.
Ensuite pour ce qui est des prédictions, il n'y en a aucunes comme ça, c'est simple.
Si on ne regarde que de très loin le CDC et le projet final, on pourrait dire que c'est plutôt décevant puisqu'il manque beaucoup de choses comme les prédictions et certains affichages.
On peut aussi se dire ça en comparant la maquette du CDC et le résultat final.
Clairement, un œil non avisé pourrait être très déçu et pourrait dire que c'est un échec.
Et moi je vais vous expliquer pourquoi, au contraire c'est un total succès.
Déjà, la beauté de l'interface est très difficile à répliquer en Windows Forms et il faudrait plus d'une semaine de travail pour arriver à quelque chose qui pourrait ressembler un tout petit peu à la maquette.
Ensuite, si on regarde bien, on a quand même une application qui nous permet de suivre les informations de la course et qui calcule des choses à notre place. C'est déjà une grosse plus-value par rapport à la page Data de la F1TV.
Et finalement, les prédictions, les affichages et le style, ce sont les choses les moins compliquées du projet. On ne se rend pas compte que pour simplement afficher les 20 pilotes dans le bon ordre, il faut énormément de travail.
Voici une petite représentation graphique de la quantité de travail nécessaire pour en arriver à l'état actuel du projet :
Pour en arriver à un affichage, il a fallu récupérer automatiquement les images en utilisant un browser headless ce qui a pris un temps fou à mettre en place et il a fallu surtout lire les informations que l'on recevait des images.
J'ai passé presque 90 % du temps de mon projet à développer 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 à la récupération des images, ou surtout à la reconnaissance de texte et de chiffres, et l'affichage est ruiné.
Si j'avais passé ne serait-ce qu'une semaine de plus juste sur l'affichage, le résultat final n'aurait rien à 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ésultat final.
Pour toutes ces raisons, je dirais que le CDC était trop superficiel, mais que l'application est conforme à l'idée générale de ce dernier et qu'il serait très facile de la rendre parfaitement conforme maintenant que tout le travail de fond a été fait et fonctionne et je pense donc que c'est un succès.
Planning prévisionnel
Mes suiveurs m'ont demandé un planning de type GANTT pour ce travail de diplôme
Je n'ai pas utilisé un logiciel particulier pour faire ce dernier, mais je me suis inspiré des principes fondamentaux d'un diagramme de ce type.
Comme l'original a été fait sur Excel, je ne peux pas vraiment l'insérer de manière lisible ici, mais il est disponible dans le dossier Planning.
Mais voici un résumé de son contenu :
Tâches
J'ai décidé de décomposer mon planning en trois grands types de tâches.
- Programmation
- Documentation
- Tests
L'idée est de permettre une meilleure lisibilité et de me permettre à moi de me faire plus facilement à l'idée de ce qu'il m'attend.
Voici la liste des tâches par rubrique :
PT
Cette rubrique contient les tâches qui n'ont pas leur place dans les trois catégories principales.
PT1 / préparation au travail de diplôme (2)
Cette tâche est un peu hors catégorie, mais c'est normal, c'est une supertâche qui regroupe beaucoup de choses. C'est une tâche qui est planifiée pour deux jours et qui normalement devrait être faite les deux premiers jours du travail.
Le but est de préparer tout ce qui peut être préparé 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âches en rapport de près ou de loin avec la documentation du projet.
DT1 Création du poster (1)
Cette tâche consiste à faire une version numérique du poster qui soit en accord avec les consignes qu'on nous a données. Le but est aussi et surtout de faire poster dont je sois fier et que je sois content de montrer.
Il y a déjà des croquis de poster et j'ai clairement prévu de travailler sur ça pendant les vacances alors, je n'ai mis qu'un jour et je l'ai placé juste avant le rendu de ce dernier.
DT2 Documentation Analyse de l'existant (2)
Cette tâche est dédiée à l'écriture de la documentation et plus précisément de l'analyse de l'existant.
Comme il y a pas mal de technologies utilisées dans mon projet, j'aimerais faire correctement un vrai debrief de pourquoi j'ai utilisé l'une ou l'autre, alors, j'ai assigné deux jours dessus.
DT3 Documentation Analyse organique (5)
Cette tâche est la plus grosse dans la catégorie 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âche que je vais devoir détailler exactement comment fonctionne chaque partie du projet.
Ces cinq jours sont éparpillés sur le projet en général à la fin du développement de chaque grande partie de projet. Le but est de ne rien oublier et de ne pas avoir à tout faire en même temps.
DT4 Documentation Analyse fonctionnelle (2)
Cette tâche est déjà moins grosse, elle consiste à documenter le fonctionnement de l'application et comment utiliser les composants que j'ai développés.
Je l'ai mis en fin de projet, car comme j'ai l'habitude de faire des analyses fonctionnelles plutôt précises, le moindre changement dans l'UI peut tout rendre obsolète.
J'y ai mis deux jours, puisque j'aimerais correctement documenter avec de bonnes photos et scénarios pour qu'on puisse voir toutes les possibilités de l'application.
DT5 Documentation Tests (1)
Cette tâche est un peu plus petite qu'elle ne le devrait. Elle concerne la documentation des différents tests. Je n'y ai mis qu'un seul jour, car en réalité les différentes tâches de tests contiennent aussi beaucoup de documentation,
DT6 Documentation Reste (2)
Cette tâche est une tâche un peu vague. Elle contient toutes les actions autres que j'aurai besoin de faire (Mise au propre, orthographe, génération de PDF ...). J'y ai mis deux jours pour avoir un peu de marge, car ce sont toujours des tâches qui paraissent faciles, mais qui à la fin prennent beaucoup de temps si on les fait bien.
PT
Rubrique programmation qui contient toutes les tâches qui touchent à la programmation et au développement de l'application.
PT1 Programmation récupération des images (3)
Cette tâche est estimée à seulement trois jours, il ne faut pas s'y méprendre, c'est une des tâches les plus dures et lourdes au niveau de la documentation et en explications. Cependant, un POC (Proof Of Concept) assez avancé a déjà été fait et donc cela permet de n'envisager que trois jours, car il suffit de l'implémenter et de la peaufiner.
Cette tâche consiste à prendre en entrée 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'écran et ne demandant pas à l'utilisateur de copier-coller quoi que ce soit où de donner ses identifiants F1TV c'est un challenge.
Cela peut paraitre curieux alors de mettre cette tâche loin dans le planning même si c'est la première étape du projet. Encore une fois, cela s'explique avec le fait qu'il y a déjà un POC qui fonctionne à peu près et que donc préfère commencer avec des tâches plus incertaines dans le cas où elles prendraient plus de temps que prévu.
PT2 Programmation OCR (5)
Cette tâche consiste à développer la partie qui reconnait le texte sur les images. C'est très certainement la tâche qui risque le plus de déborder, car c'est celle qui est la plus complexe techniquement puisqu'elle demande non seulement la lecture sur image, mais aussi le développement d'algorithmes de traitement de cette donnée pour être sûr qu'elle a bien été lue.
J'y ai ainsi alloué cinq jours, mais j'espère que j'arriverai à 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ésultat.
PT3 Programmation, stockage et modèle (5)
Cette partie est moins technique, mais concerne le stockage des données que nous retourne la lecture des images. Mais elle va demander de la réflexion et de l'intelligence de programmation, car il faut que cette partie anticipe les besoins de la vue et prépare un terrain fertile qui ne demande pas un refactor quand on passera au développement de la vue.
C'est pour cela que je lui ai aussi assigné cinq jours de travail et elle doit absolument être commencée après la lecture.
PT4 Programmation Vue de l'APP (5)
Cette partie peut être horrible comme très facile, cela dépend complètement de la qualité du travail avant. Si le modèle est parfait et que les données sont intègres, cela devrait être plutôt simple de les afficher de manière intéressante. Cependant, cette partie débordera sûrement un peu, car tout le temps gagné avec de bonnes données sera utilisé pour tenter de faire de la prédiction.
Pour ces raisons, je lui ai assigné également cinq jours de travail et elle doit absolument être faite après le modèle.
PT5 Programmation mise en commun (3)
Cette tâche est aussi un petit peu spéciale, car elle regroupe plusieurs choses. En gros, chaque partie de programmation sera assurément assez indépendante et il faudra à un moment faire un seul projet C# qui contient tout.
Il est difficile d'estimer à quel point cela va être compliqué alors, j'ai été conservateur et j'ai mis trois jours.
TT
Cette rubrique contient les tâches qui sont uniquement des tests. La plupart des tâches de programmations contiennent déjà des tests, mais certaines demandent une attention particulière.
TT1 Tests OCR (2)
Cette tâche est une des tâches les plus importantes. Son but est de faire un protocole de tests complet qui permette de comparer les différents algorithmes de reconnaissance de texte.
Je l'ai mise après la reconnaissance, mais même maintenant en écrivant ces lignes, je me dis que dans le planning effectif, elle sera faite pendant la tâche de programmation. En effet, comment savoir si mon tout nouvel algorithme est réellement mieux que le précédent.
Je prévois deux jours, car je pense que faire le dataset va prendre beaucoup de temps, il faut prévoir un certain nombre d'images et de texte qui pourront ensuite être données sous forme de tests. C'est certes une tâche de test, mais c'est aussi de la programmation.
TT2 Tests finaux (2)
Cette tâche de tests est un peu vague, elle regroupe les différents tests pour vérifier que les données sont bien affichées correctement. Ce qui serait cool si j'ai du temps en fin de travail de diplôme serait de faire un système de test qui permet d'entrainer le programme à mieux reconnaitre certaines choses comme des arrêts aux stands si on lui donne les trois dernières années de grands Prix.
J'ai mis une durée arbitraire de deux jours, mais je ne sais pas vraiment combien de temps cela va vraiment durer. Elle est évidemment à effectuer une fois que tout est à peu près terminé.
Planning effectif et différences
Alors !
Ces lignes sont écrites dans les derniers jours du travail de diplôme et j'ai des choses à dire.
Premièrement, je suis plutôt content de mon estimation du travail. Je trouve que j'ai bien estimé la quantité de travail et combien de temps les différentes tâches allaient prendre. La plupart des dépassements sont des imprévus et/ou des allers et des retours entre d'autres tâches.
La raison pour laquelle je suis plutôt content de ma planification, c'est que malgré l'usine à Gaz que représente ce projet et le nombre de soucis que j'ai eu, j'ai quand même pu arriver à un projet qui fonctionne en suivant essentiellement fidèlement le planning. Une chose dont je suis assez fier, c'est la documentation. En ayant développé le squelette de l'app dès le début du projet, ça m'a permis d'avancer au fur et à mesure du projet la conscience tranquille.
Bon, c'est bien joli les fleurs, mais clairement, c'est loin d'être parfait. Au moment de la planification, je n'avais pas prévu de faire des allers et des retours entre plusieurs tâches. 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-être dû inverser l'ordre.
Mais il y a deux gros soucis dans mon planning :
- L'ordre des tâches n'était pas bon (mais il a été décidé comme ça pour que les plus grosses difficultés soient faites en premier) ce qui a créé pas mal de soucis. Ex : L'émulateur de la F1TV a été fait très tard et finalement les images récupérées n'étaient pas de la même qualité que ce que j'avais prévu en développant l'OCR en premier.
- Les Tests ont été négligés et utilisés comme des jours tampons. Ça, c'est la plus grosse erreur de planning. Autant les autres sont pénibles, etc. mais n'ont pas forcément compromis la bonne réalisation du projet alors que les tests ont été mal placés et ont finalement été balayés tandis que s'ils avaient été mieux planifiés ça ne serait pas arrivé.
Solutions :
L'ordre des tâches a été décidé exprès de cette façon pour éviter de prendre trop de risques. L'idée était qu'en faisant le plus dur au début, je pourrai facilement changer le cahier des charges. J'ai envie de dire que j'aurais dû être plus confiant, mais pour être honnête, je pense que c'était un mal pour un bien. Je ne pense pas avoir "bien" fait, mais je pense que c'est une erreur qui était 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étail dans la partie test de la documentation, mais je vais résumer un peu ici. La documentation a été faite dès le début du projet. J'ai mis en place le squelette pour qu'ensuite, il soit simple d'y ajouter au fur et à mesure. J'aurais dû faire exactement pareil avec les tests. Si j'avais fait au moins le squelette des tests au début du projet, j'aurais pu beaucoup plus facilement en faire et cela m'aurait fait gagner un temps fou et j'aurais même pu faire du TDD (Test Driven Developpement). Je suis persuadé que cette bête erreur de planification m'a coûté très cher, car ne pas avoir une bonne stratégie de tests a dû me faire perdre un temps fou.
Pour conclure, je suis content parce que j'ai réussi à 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é
Analyse Organique
Outils utilisés
Visual Studio 2022
C'est l'application que j'ai le plus utilisé, 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 à dire à ce sujet à part que c'est un outil qui marche bien et qui est gratuit si on prend la community édition.
Visual Studio Code
Cet outil est déjà un peu plus intéressant. C'est le second outil que j'ai le plus utilisé. J'en ai surtout eu besoin pour écrire de la doc, mais aussi pour coder en python et pour contrôler mkdocs.
Visual Studio est un IDE absolument génial qui est très puissant avec les bonnes extensions. Je l'utilise au quotidien pour tout ce qui est développement WEB, Mobile ou pour éditer des fichiers de configs pour mes drones ou imprimantes 3D. Je peux même compiler le firmware pour ces dernières en utilisant une extension faite pour.
Les possibilités de customisation sont presque infinies et c'est un plaisir d'utiliser ce logiciel gratuit fourni par Microsoft, mais qui est amélioré constamment par des développeurs indépendants.
Je conseille à n'importe quel développeur de l'essayer à moins qu'il soit uniquement sur C# ou il serait plus intéressant d'utiliser visual studio 2022.
Material/Mkdocs/Markdown
Pendant ce projet, j'ai utilisé exclusivement du Markdown avec l'aide de Mkdocs et Materials.
Le choix de Markdown a été plutôt simple, c'est une façon facile et efficace de créer de la documentation et on n'avait pas le choix de l'utiliser.
On avait également l'obligation (Ou au moins un très 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éable 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ément du genre à aller changer toutes les couleurs et polices pour avoir la doc parfaite, j'ai préféré passer du temps sur mon app. Mais même si ces outils offrent une customisation très avancée, il est très facile de créer un projet simple et j'aime beaucoup cette simplicité.
J'ai eu pas mal d'aide de la part de M. Briard pour implémenter certaines features et je l'en remercie très chaudement, car sans son aide ce document serait sûrement un peu moins commode à lire (Oui oui ça aurait pu être pire, je sais, c'est dur à imaginer).
Figma
Figma est l'outil que j'ai utilisé pour créer mon poster et un certain nombre des diagrammes de cette documentation.
J'utilise aussi cet outil dès que je vais faire des maquettes de sites ou d'applications. D'ailleurs les maquettes dans le cahier des charges ont été faites avec.
C'est un outil en ligne parfaitement gratuit qui conserve tout dans le cloud. Franchement, je n'ai rien à dire, je n'ai pas utilisé plus de 15 % des features que cet outil propose et je suis déjà conquis.
Technologies utilisées
Dans ce projet, différents choix ont été faits pour ce qui est des technologies.
Certaines ont été choisies, car elles étaient 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ésumer ici ces choix, mais je reviendrai sur la plupart d'entre eux plus tard quand j'explique ce que je fais avec.
Selenium
Selenium est une librairie à la base Node JS qui permet d'automatiser des actions sur un navigateur internet. Le but premier et je pense son utilisation première 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é, 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égorie. Le "Scrapping" c'est l'acte d'aller récupérer des informations sur des pages web automatiquement pour alimenter sa propre base de données. En effet, si on arrive à passer les protections anti-bot, on peut facilement utiliser Selenium pour scraper tous les sites qui nous passent par la tête.
Le cahier des charges que j'avais en tête en cherchant une technologie de contrôle de navigateur internet était le suivant :
- Simple
- Permettant de contrôler un navigateur Headless (Voir chapitre "Simuler un navigateur ?")
- Permettant de contrôler Firefox
- Ayant un wrapper C#
- Permettre de changer certaines choses comme les cookies en direct
- Permettre d'interagir avec les éléments d'une page
- Fonctionner
Simple, car je ne voulais pas avoir à passer trop de temps dessus (ça n'a pas bien vieilli lol...). Je voulais que l'on puisse utiliser Firefox parce qu'il n'implémente pas les mêmes sécurités que Chrome pour faire simple. J'avais besoin que la lib puisse contrôler 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é 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 éléments de la page pour naviguer dessus et d'insérer des cookies pour me connecter sans avoir à passer par le login de la F1TV qui est très bon pour détecter les bots.
Avec un cahier des charges pareil beaucoup de librairies ont été abandonnées. J'ai pu tester pleins de librairies C# qui arrivaient à contrôler un Chrome et même pas mal qui arrivaient à contrôler un Chrome Headless. Mais le choix est très vite restreint quand on veut pouvoir contrôler Chrome OU Firefox.
À la base, mon choix, c'était porté sur Puppeteer Sharp qui est une librairie qui se veut être exactement ce que je veux.
Je voulais utiliser cette librairie, car il y a des plugins qui sont très orientés scrapping, en effet, ils implémentent de nombreuses techniques pour permettre de mieux passer inaperçu par les systèmes de détection de bots.
Sur le papier, c'est la librairie parfaite qui correspond parfaitement au cahier des charges que je m'étais fixé 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èmes :
- Toutes les versions de la librairie ne fonctionnaient pas. Il fallait faire des tests avec différentes versions de la librairie et de ses dépendances simplement pour faire lancer un browser. Et ça, c'est quand ça marchait, car il y avait des jours où des machines sur lesquelles je n'ai juste pas pu faire fonctionner la librairie.
- Même avec les techniques proposées par les plugins "Stealth" je n'arrivais pas à bypass les sécurités de la page de login de la F1TV. J'ai essayé tout ce que j'ai pu trouver sur internet, mais on se fait toujours chopper dès que l'on arrive sur la page.
- Et le pire de tous, impossible de faire fonctionner une vidéo. J'ai pu faire tout ce que je voulais faire finalement en passant par l'utillisation de cookies pour la connexion. Tout ça pour arriver au moment où il faut lancer la vidéo, et là, crash. Impossible de faire fonctionner Puppeteer Sharp avec une vidéo. Dès qu'elle se lance, c'est un crash assuré sans message d'erreur clair. Et le souci, c'est que le wrapper C# n'est pas vraiment bien supporté 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û abandonner cette librairie, ce qui a été très dur, car j'avais passé beaucoup de temps dessus à essayer de la faire marcher.
Ensuite le choix de Selenium était plutôt simple, c'était la seule option restante. À ce jour, je ne connais aucune autre librairie que Puppeteer ou Selenium qui puisse contrôler un Firefox Headless en respectant mon cahier des charges et qui soit donc disponible depuis C#.
Si je n'arrivais pas à faire fonctionner Selenium, j'aurais dû abandonner l'idée 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ême si la version C# n'est vraiment pas bien documentée, la plupart des documentations de la version JS sont pertinentes pour la version C# même si ça n'est pas la même syntaxe.
Pour résumer, j'ai choisi Puppeteer car c'était la seule option viable pour mon besoin.
(Note : Par contre si je trouve la personne chez Mozilla ou puppeteer qui a décidé d'hard coder la résolution 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ÈS MAL SE PASSER)
CSharp
Je pense que c'est le choix le plus simple à expliquer. C# est un langage de programmation orienté objet relativement haut niveau qui a été créé par Microsoft et qui a comme cible le développement d'applications pour Windows. (On peut évidemment trouver des adaptations pour le faire tourner sur Linux, mais ce n'est pas vraiment le but du langage)
En plus d'être 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 à l'aise que pour d'autres langages comme le Python ou le JS.
Mon but n'était pas de faire une application Web et je travaille sous Windows. Je savais que mon projet allait demander un minimum de programmation orientée objet. J'ai ainsi immédiatement pensé à utiliser C#.
Cependant, j'aurais très bien pu utiliser un langage comme python qui m'aurait clairement facilité la tâche avec des librairies bien plus fournies et plus souvent mises à jour. Mais comme je ne suis pas du tout aussi à l'aise avec, je pense que le C# était la meilleure option. Mes seuls regrets après coup sont que je trouve les Windows Forms très moches et qu'il est particulièrement difficile de les rendre plus jolies et que les librairies disponibles en C# pour des scénarios très précis ne sont pas au niveau de celles pour JS et pour Python. Cependant, si j'avais à refaire le projet, je reprendrais C# je pense.
Python ?
Alors ce choix-là est plus compliqué à comprendre.
Pour tout le projet, j'ai tenté de garder le C# comme langage et de ne pas utiliser autre chose. Cependant, j'ai dû utiliser une seule fois le Python dans un cas très précis.
Je n'aime vraiment pas coder en python de base et clairement, j'aurais préféré ne pas l'utiliser, mais je n'avais pas le choix.
Le besoin dans le cas du python était le suivant :
J'avais besoin d'un moyen de récupérer des strings et les décoder avec une clé encodée avec le système propriétaire de Windows d'encodage.
Le souci, c'est que j'avais avec le C# c'est que les méthodes de décryptions ne fonctionnent pas pareil qu'en python et tous les exemples que je pouvais trouver étaient en python. J'ai essayé pendant un sacré moment de faire fonctionner la décryptions en C# mais sans succès.
J'ai donc directement utilisé le python comme faisait toutes les personnes que je pouvais voir sur internet et je pense que ça n'est pas une mauvaise idée. En effet, cela veut dire que si à un moment Chrome est mis à 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ême qu'il y a beaucoup plus de gens qui codent dessus, et pour ce genre d'utilisation très spécifique, c'est plutôt pratique.
Le seul problème, c'est que cela oblige l'utilisateur à avoir python installé sur sa machine et que sa version doit être compatible... (les joies de python).
Firefox
J'en parle déjà plus bas, mais le choix de navigateur est super important.
Déjà tous les navigateurs n'ont pas un mode Headless (sans tête, mieux expliqué dans la rubrique "Simuler un navigateur ?"). Par exemple, même si Edge est maintenant basé 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ès peu supportent Firefox ou Edge. Donc, il me fallait un navigateur qui puisse opérer en Headless et qui soit supporté par plusieurs librairies d'automatisation.
Il n'y a que Firefox et Chrome qui soient conformes à ces exigences. (Je n'ai pas vérifié pour TOUS les navigateurs. Peut-être que les Opera GX ont aussi un mode headless super, mais je me suis concentré sur les navigateurs plus grand public).
Chrome est supporté par plus de lib, mais le souci c'est que la F1TV utilise un lecteur de vidéo avec DRM (Plus d'infos là-dessus dans la partie "Simuler un navigateur ?") et donc le choix était simple. Il ne restait que Firefox.
Tesseract
Je pense que le choix le plus simple après le C# fut l'utilisation de Tesseract.
C'est tout simplement l'outil le plus utilisé pour faire de l'OCR. À la base, c'est une lib Python (Ah tiens encore ?) qui peut être redoutablement efficace avec le bon dataset.
Il existe d'autres outils, mais j'ai décidé de prendre celui-là à cause de son support juste incroyable et de son omniprésence dans la documentation OCR.
En plus il est facile à utiliser et je ne pense pas encore avoir fait le tour de tout son potentiel dans ce projet.
Fonctionnement général
Avant de passer à l'explication de chaque partie du projet en détail, je pense qu'il est important de faire un petit point sur comment toutes les parties du projet s'emboitent et fonctionnement ensemble. Comme ça, quand vous lirez l'explication d'une étape, vous serez conscient de à quoi elle sert, et où elle s'inscrit dans le projet principal.
Les briques principales
Voici trois grosses étapes du projet. Pour rappel, ce sont des vulgarisations plutôt larges qui n'ont qu'un seul but, aider à la compréhension de ce qui vient par la suite.
Récupération d'images
Pour faire simple, on peut voir qu'il y a deux parties à cette étape. La première en partant du haut représente un script python qui va chercher des informations dans la base de données de Chrome qui est en SQLite. Ces informations dans notre cas sont les cookies de connexion.
Dans la seconde étape, 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éresse et qu'une des infos que l'on récupère est une image de la page en format PNG que l'on envoie au programme C#.
Ces deux parties sont liées, car pour se connecter à la F1TV Selenium a besoin des cookies de connexion récupérés par le programme Python.
La première partie est un processus qui n'est utilisé qu'une seule fois au démarrage tandis que la récupération d'images et en continu pendant toute la durée de l'utilisation de l'application.
OCR
On peut voir dans ce diagramme simplifié qu'avec l'aide de ce que contient le fichier "Config.JSON" on découpe l'image que l'on a récupéré au préalable 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écupère les informations sous forme de texte et on le renvoie dans le programme C#
Dans cette partie explicative générale, on ne reviendra pas sur la création de ce fichier config. Pour plus d'infos à son sujet, voir la rubrique (OCR/Fonctionnement général)
Traitement et affichage
On peut voir dans ce dernier mini diagramme simplifié qu'on prend les données que l'on récupérait de l'étape précédente qui ne sont pas forcément toutes cohérentes et qu'on les traite pour leur redonner du sens avant de les stocker dans une base de données SQLITE. Ensuite cette même base de donnée fournis les infos nécessaires pour des affichages (Ces affichages sont directement récupérés depuis le projet en cours de fonctionnement).
Résumé du fonctionnement général
Ce dernier diagramme est un schéma fait pour représenter de la manière la plus simple possible toutes les briques du projet et comment elles s'imbriquent ensemble. La représentation est un peu différente des trois autres diagrammes, car le but ici est surtout de montrer le chemin que fait la donnée à 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écupération d'images, le python va récupérer les cookies dans la base de données chrome pour ensuite les retourner à Selenium.
Selenium va ensuite pouvoir lancer un navigateur (en l'occurrence Firefox) et utiliser les cookies récupérés pour aller sur la page de la F1TV qui va retourner un certain nombre d'infos à Selenium.
L'info qui nous intéresse 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écoupe selon les directives données par le fichier Config.JSON (il renseigne les zones à découper et ce qu'elles représentent) et après le découpage, on se retrouve avec une zone principale, vingt zones de pilotes et 9 fenêtres de données par zone de pilote donc 180 fenêtres en tout.
Ces fenêtres sont ensuite envoyées pour être filtrées (retirer le flou, mettre en évidence les caractères, en gros les préparer pour la reconnaissance) dans la partie OCR
Dans cette partie, après avoir filtré les images, on les envoie à Tesseract pour qu'il nous retourne des résultats d'OCR. Ces résultats sont ce que Tesseract a trouvé sur les images et ils sont retournés sous la forme de Data Pilote. Ex (Position : 1,Tour : 45, Temps au tour : 1:34.683, Pneus : Medium etc....)
Finalement, ces données ont envoyées dans la partie traitement qui va faire des vérifications d'usage pour s'assurer qu'elles sont correctes et quand c'est fait, tout est envoyé dans une base de données SQLite.
On ne montre pas non plus dans ce diagramme la parte affichage des données, car je ne trouve pas pertinent de l'inclure ici.
Et voilà, c'est le fonctionnement très général et simplifié de l'application. Je vous invite à continuer à lire cette documentation pour des informations plus précises à propos de toutes ces étapes. Bonne lecture !
Récupération des images
Voici la première grande étape du projet.
Pour rappel, Amazon héberge directement le site de la F1TV et possède les droits sur les données de la F1. C'est sous le nom de AWS (le service d'hébergement d'Amazon) que la firme apparait en tant que sponsor.
On peut voir ce nom apparaître assez souvent quand on regarde un Grand Prix, car comme ils ont la mainmise sur les données, ils peuvent insérer des bandeaux d'informations sur le flux public sur ce qu'il se passe, voir même faire des prédictions (Bien qu'un peu bancales)
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ées en direct pendant un Grand Prix. Ils ont dû dégotter un juteux contrat pour s'occuper de toute l'infrastructure digitale de la F1 (du moins publique) en échange d'une exclusivité totale sur certaines choses comme les Data.
Évidemment, je ne fais que conjecturer et ce que j'ai dit n'est pas à prendre au pied de la lettre, mais c'est une explication possible, je pense, de pourquoi il est si difficile de trouver des données sur la F1 facilement en temps réel.
Il existe bien quelques API un peu bancales publiques, mais le problème, 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'être sûr que les données 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ètement vrai. En effet, depuis que je possède un abonnement à la F1TV, il existe une source d'informations très précieuse qui m'aide énormément 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éo, différentes informations capitales sur la course.
Le problème, c'est que comme je viens de le dire, ces données ne sont pas accessibles comme un tableau HTML ou un flux RSS ou un tableau JSON. C'est un flux vidéo. Il faut savoir qu'entretenir une diffusion de flux vidéo en 1080P pendant deux heures accessible par des milliers d'abonnés est EXTRÊMENT cher, surtout quand on le compare à simplement afficher les données dans un tableau. Ce qui veut dire que ce choix est délibéré et a un sens au niveau économique. Je pense donc que c'est justement pour éviter que des petits malins puissent juste venir scraper l'intégralité des données 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ées ne sont pas faciles à avoir qu'elles sont IMPOSSIBLE à avoir. Et c'est là que ce projet entre en jeu. Mais pour décoder les données d'une image, il faut d'abord ... (roulement de tambours) ... Avoir des images !
Et c'est là que vient se glisser cette partie du projet.
Comment faire ?
Le but de ce segment est de se concentrer sur la récupération et la mise à disposition, pour le reste du programme, des images en direct de la F1TV dans la meilleure qualité possible et dans les meilleurs délais.
Pour ce faire, il y a plusieurs solutions :
- Reverse engeneer la F1TV pour accéder directement au flux sans passer par la plateforme internet et pouvoir prendre images à volonté.
- Avoir tout simplement une page de la F1TV ouverte sur un écran et prendre des screenshots à intervalles réguliers.
- Simuler un navigateur internet sans qu'il soit affiché et le contrôler automatiquement pour qu'il prenne des captures.
La première option aurait été la plus élégante, mais lors d'un POC que je tentais de réaliser, je me suis rendu compte que cela serait un peu trop compliqué et long à faire. Sans compter le fait que les rediffusions de Grand Prix ne sont pas gérées de la même manière que les diffusions en direct. Et que pour faire des Tests en direct, il faudrait attendre à 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ée dans la case "Trop dur, Trop chiant, Sûrement illégal" (Oui, je sais, c'est une catégorie bien spécifique, mais c'est ma documentation, je fais ce que je veux).
La troisième option aurait été la plus simple (et moins drôle) et je suis presque sûr que je peux implémenter cette dernière en moins d'une après-midi. Sauf qu'elle apporte de gros soucis.
- On ne peut pas garantir l'intégrité et la continuité des données si l'utilisateur avance ou fait pause, même par simple inadvertance.
- La moindre fenêtre qui s'afficherait devant ruinerait toute la reconnaissance de caractères.
- On ne peut pas contrôler la qualité du flux et on est obligé de faire confiance en l'utilisateur
- On ne peut pas vraiment automatiser quoi que ce soit niveau tests ou même pour faire du scrapping auto pour remplir une base de donnée.
- Et finalement le pire inconvénient : C'EST NUL ! Je ne pourrais jamais utiliser un projet qui fonctionne de cette façon, je ne peux pas me permettre d'avoir un écran inutilisable quand je commente et auquel je dois constamment faire attention pour ne pas perturber la reconnaissance. Pour moi, cette option aurait été celle à choisir en cas d'extrême urgence et en dernier recours, car le projet deviendrait inutile.
J'ai donc décidé de m'occuper de la seconde option : Simuler un navigateur.
Cette option, bien que complexe et difficile à implémenter, propose une solution à tous les problèmes et permet une récupération quasi sans compromis.
Simuler un navigateur ?
Simuler un navigateur internet n'est pas forcément très difficile. Chromium par exemple offre une panoplie d'outils natifs et énormément de librairies existent permettant de facilement et en quelques lignes simuler un Google Chrome et le contrôler sans afficher son UI (Interface Utilisateur).
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é et surtout qui implémente 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'écran, 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 à nouveau). Ce qui dans notre cas est un immense problème. Mais Firefox ne nous bloque pas de cette façon et il est donc assez facile de passer outre.
L'explication sans trop rentrer dans les détails est la suivante :
Dans Chrome, le player par défaut utilise une technologie appelée "PCP" ou "Protected Content Playback" qui leur permet de bloquer au moins une partie des techniques de récupération du flux vidéo et audio.
Cependant, Firefox de pas sa nature Open Source utilise "Open H264" pour lire ces mêmes flux soumis à des DRM et Open H264 n'implémente pas les mêmes restrictions.
Sauf que Firefox n'est pas aussi facilement émulé que chrome et cela réduit notre choix de librairies à ... Une seule… Qui est Selenium. (Il existe aussi Pupetteer C# mais j'ai rencontré énormément de soucis avec cette dernière dès que je voulais lancer une vidéo)
Mais même si la documentation est plutôt maigre parfois, c'est une bonne librairie qui permet de très bien contrôler une instance de chrome ou de Firefox.
Contrôler le navigateur
Maintenant que l'on sait quel navigateur simuler et avec quelle technologie, on peut passer à la réalisation.
Ce qu'il y a de bien avec Selenium, c'est qu'on a un certain nombre de commandes très haut niveau qui nous permettent de contrôler un navigateur de manière plutôt précise.
Je vais décrire ici la procédure habituelle utilisée 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 à 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écupérer des images de la F1TV :
- Démarrer une instance de navigateur avec les bons arguments
- Ajouter les bons paramètres pour ne pas se faire flag comme un bot
- Naviguer sur la page de la F1TV
- Ajouter les cookies de connexion pour avoir accès au contenu de la page
- Naviguer sur la page du Grand Prix demandé
- 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 à la DATA CHANNEL
- Appuyer sur Espace pour faire apparaitre le bouton d'accès au paramètres
- Cliquer sur le menu déroulant des résolutions
- Trouver l'option 1080P et la sélectionner
- Cliquer sur le bouton qui met la vidéo en plein écran
- Prendre de screenshots à intervalles réguliers
Pour faire toutes ces actions, on doit récupérer les éléments selon leur ID ou leur classe.
Voici un exemple qui récupère le bouton de plein écran et qui clique dessus :
IWebElement fullScreenButton = Driver.FindElement(By.ClassName("bmpui-ui-fullscreentogglebutton"));
fullScreenButton.Click();
Ça peut paraître plutôt simple dit comme ça et quand tout fonctionne ça l'est, mais la difficulté vient du fait qu'à peu près n'importe laquelle de ces étapes peut rater et qu'il faut donc faire un bon système de gestion d'erreurs qui puisse aider l'utilisateur en cas de problème.
Parfois, il est aussi difficile de trouver un élément 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égulières, ce qui n'est pas compliqué en soi, mais ce sont toutes ces petites choses qui rendent le processus long à mettre en place.
Il faut dire aussi que les sites ne sont pas forcément très contents de voir des bots passer, car cela peut être un risque de DDOS et de Scraping (Comme moi) et donc ils mettent en place des systèmes pour nous empêcher de faire ce que l'on veut.
On peut utiliser différentes techniques pour passer outre ces restrictions comme :
- Changer son User Agent
- Changer sa résolution
- Ne pas avoir des patterns trop prévisibles
- 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ètes
J'ai eu l'occasion de tester toutes ces méthodes pour tenter de passer derrière les radars de la F1TV et visiblement, j'ai réussi pour les pages principales, mais pas pour les pages de Login.
Il faut savoir que la bataille entre bots et propriétaires de sites est un grand jeu du chat et de la souris et que les plateformes innovent constamment leur sécurité. Et il se trouve que la partie login de la F1TV est hébergée autre part que le reste du site chez Amazon et qu'elle possède les meilleures sécurités que j'aie pu voir. Aucunes des méthodes que j'ai citées et d'autres encore que j'ai essayé n'ont réussi à fourvoyer le système.
J'ai donc été obligé de faire appel à la connexion par Cookies pour pouvoir accéder au reste du site internet.
Récupérer les cookies ?
Alors, on va mettre de côté toutes les questions de sécurité et de violation de la vie privée et de protection des données des utilisateurs pour ce chapitre. Car pour faire simple, je siphonne TOUS les cookies de la personne qui utilise mon app.
Alors évidemment ça n'est pas pour faire des bêtises avec et c'est pour une "bonne" raison, mais bon quand même ça peut faire bizarre comme ça.
Je pense que vous savez déjà ce qu'est un Cookie, mais je vais malgré tout faire un petit point là-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és sur la session.
Cependant, si on quitte le site ou que l'on ferme le navigateur, le site ne peut pas garder en mémoire que c'est bien vous quand le lendemain, vous retournez dessus. Pour palier à cette limitation, on a inventé cette chose magnifique (hem...) que sont les cookies !
Les cookies sont des petits fichiers qui sont stockés dans votre navigateur et qui peuvent servir à beaucoup de choses comme traquer votre activité sur internet et espionner un peu ou aussi par exemple, servir de jeton de connexion.
L'idée 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.
Ça peut paraître génial, et c'est effectivement bien pratique, cependant ce n'est pas sans risques. En effet, imaginons qu'un acteur malveillant parvienne à s'emparer de ces petits fichiers, il pourrait ainsi facilement se faire passer pour vous. Alors un cookie expire à un moment donné pour tempérer les risques, mais ils sont toujours présents.
Dans notre cas, on peut vite comprendre pourquoi cela peut être intéressant de récupérer 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 à la F1TV et aller prendre des photos directement sans que l'utilisateur ait à faire quoi que ce soit.
Sauf que les cookies ne sont pas stockés en clair comme ça. Évidemment, Google Chrome a mis en place quelques techniques pour éviter que n'importe qui puisse s'amuser à aller taper dans les cookies de la machine.
Tous les cookies sont stockés dans une base de données SQLite avec les noms en clair et les valeurs sont encryptées en utilisant la méthode AES 256 qui est une méthode de cryptage très utilisée et efficace.
Tellement efficace qu'il serait complètement inutile de tenter de les décrypter en utilisant de la force brute pour trouver la valeur ou même une attaque de dictionnaire ou quoi que ce soit.
Si ces valeurs peuvent être encodées et décodées en local sur la machine sans connexion internet, cela veut dire que la clé est stockée sur la machine. Et si je peux mettre là, mais sur cette clé, alors je pourrai lire tous les cookies de la machine.
Cette clé est stockée dans les fichiers de Google Chrome sous Google\Chrome\User Data\Local State. Et dans ce fichier, on peut trouver une liste de données en clé valeurs et on peut trouver la clé sous os_crypt encrypted_key. On pourrait croire que l'on a déjà touché le jackpot, mais il reste encore une étape. Cette clé est cryptée en utilisant le système d'encryption de Windows. Cette encryption est utilisée pour empêcher des utilisateurs non connectés d'accéder à certaines données. Mais comme nous sommes connectés, nous pouvons facilement utiliser les librairies de décryptions pour trouver la valeur de cette clé.
Et à partir de là, il suffit d'utiliser cette clé pour décrypter tous les cookies de la machine pour aller chercher ceux qui nous intéressent.
Voici un exemple du code python qui permet d'aller chercher la clé 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 été fait pour trois raisons :
- Le python est un language que je n'aime pas particulièrement mais qui possède un éventail de librairies absolument fantastique. Et pour ce genre de choses qui demandent une constante mise à jour des librairies et qui sont un peu niches le python est une option juste géniale.
- Comme c'est une des parties qui est le plus suceptible de changer vu que Chrome change relativement souvent le système de stockage des cookies. Dans une optique de facilité 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éussi à trouver de librairies C# qui me donne des résultats identiques à 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 écrit 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’aie rien à faire. On a tous les ingrédients pour automatiquement récupérer 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 à la partie OCR. Mais que nenni !
Le gros souci de l'OCR c'est que sa précision est grandement réduite dès que l'on augmente la taille de la zone de recherche. Même 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ême et qu'on tente l'OCR, on va avoir de résultats bien moins bons.
Et puis il faut aussi voir que selon les données que je cherche, je ne peux pas faire le même traitement.
Par exemple, savoir si le DRS est allumé, 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 à 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écoder.
Il faut donc faire une calibration qui puisse donner toutes les infos importantes, mais qui en même temps soit facile à utiliser, car un utilisateur doit être capable de le faire assez facilement.
Voici la liste des informations que l'on doit récupérer :
- La liste des pilotes présents 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 été de retirer le plus d'étapes possibles à l'utilisateur. Techniquement, j'aurais pu faire une version complètement manuelle, mais ça aurait pris trop de temps, alors il y a des systèmes qui permettent de rendre cette tâche moins pénible.
Liste des pilotes
Pour la liste des pilotes, j'ai pensé à utiliser une API externe pour avoir une liste dans laquelle on pourrait sélectionner des noms de pilotes, sauf que j'ai abandonné l'idée, car je trouvais que le projet avait déjà bien assez de points qui dépendent de l'extérieur.
Il y a donc une liste de pilotes dans laquelle on peut ajouter ou supprimer des noms de pilotes. L'idéal serait de mettre tous les pilotes de réserve, comme ça 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èrement manuel, on attend de l'utilisateur deux points x, y sur l'image pour ensuite avoir une idée d'où est censé se trouver la zone.
Zones pilotes
C'est là que ça devient intéressant. L'utilisateur n'a pas besoin de faire quoi que ce soit pour que le programme sache où sont les zones des pilotes.
J'aurais pu le faire manuellement en faisant choisir à l'utilisateur de donner deux points qui correspondent à la première zone et extrapoler pour en avoir 20. Sauf que si l'utilisateur n'est pas précis au pixel près (et même comme ça parfois) le vingtième pilote se retrouve avec une zone complètement désaxée.
Là, le programme va "simplement" effectuer une reconnaissance de texte sur toute l'image. Les résultats ne nous intéressent pas vraiment, tout ce que l'on veut, c'est la position des textes.
Avec les positions, il est facile de déterminer où sont toutes les zones de pilotes et donc sans que l'utilisateur ait à toucher quoi que ce soit, dès qu'il a donné les infos pour la zone principale, les zones de pilotes sont déterminées.
Voici un exemple du code utilisé 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énible pour l'utilisateur, il doit sélectionner manuellement les positions des fenêtres de données. Ensuite, dès que l'utilisateur a donné 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érent comme je l'ai dit plus tôt. Voici des exemples concrets :
Il est important que toutes ces zones soient transmises avec le plus de précision 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ée pour qu'il puisse ensuite la réutiliser pour tous les autres Grand Prix de l'année.
Le stockage est fait sous format JSON et est fait pour que le programme d'OCR puisse lire dedans toutes les infos nécessaires.
Cela fait des fichiers plutôt gros, mais je n'avais pas vraiment le choix. J'ai testé une version avec seulement les infos de la première zone de pilote, mais avec l'interpolation, les derniers pilotes se retrouvent avec des zones clairement pas à la bonne taille.
Voici un exemple de ce à 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
}
}
]
}
[Other pilots...]
],
"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 ça. L'OCR peut démarrer dans de bonnes conditions
OCR
Maintenant qu'on a des images qui arrivent automatiquement et que l'on sait où 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é le plus tests et de refactor.
Toute la partie OCR a été développée dans un projet à part avant d'être intégrée dans le projet final.
Il faut savoir que la reconnaissance est différente selon ce que l'on cherche. Je vais donc décomposer cette partie du document en sous rubriques selon les données recherchées.
Mais avant ça, je dois expliquer certains concepts qui seront importants.
Fonctionnement général
Voici un screenshot de la page DATA de la F1TV que le programme va recevoir :
Si on regarde de loin, on peut se dire que la structure est plutôt simple, mais c'est loin d'être le cas. On peut y voir au moins quatre zones contenant de l'information dans un format différent.
Dans l'exemple ci-dessus, on peut voir trois zones, mais on aurait également pu comprendre la zone de position des pilotes autour du circuit pour faire 4.
Ces quatre zones sont très différentes et contiennent d'autres informations. Pour ce travail de diplôme, 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 ça à implémenter.
J'ai utilisé le mot "Zone" plus haut et ça n'est pas juste un mot utilisé au hasard. C'est le nom de l'objet que j'utilise pour les représenter 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'être 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ès). Elle contient la portion d'image qui la concerne et ses propres dimensions.
Le parent zone ne prévoit que de pouvoir ajouter ou supprimer des éléments des listes de zones ou de fenêtres ainsi qu'une méthode qui permet d'aller chercher toutes informations des livres qu'elle contient.
L'intérêt d'une zone est de pouvoir compartimenter une image dans des parties intéressantes au niveau de la reconnaissance, mais pas de traiter d'information.
WINDOW :
L'objet "Window" est un objet qui peut ressembler beaucoup à 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 écrites sur son image.
Toutes les Window qui héritent du parent Window peuvent implémenter une méthode qui permet de renvoyer ce qui peut être décodé sur son image. Les enfants peuvent aussi aller piocher dans les nombreuses méthodes de récupération de données contenues dans le parent Window. Il vaut mieux réutiliser le plus possible que de réinventer la roue pour chaque Window.
Une analogie un peu bancale pourrait se présenter comme la suivante :
La zone est une armoire ou une bibliothèque. Si c'est une zone qui contient d'autres zones, c'est une bibliothèque et chacune de ces sous-zones sont des armoires. Leur unique but est de contenir de manière ordonnée des objets qui eux contiennent de l'information.
Les livres ici sont les Windows. Ils contiennent de l'information et sont stockés dans des armoires et on y accède en allant dans la bonne bibliothèque et en allant dans la bonne armoire.
Dernières choses pour comprendre le diagramme :
- Il existe une Main Zone qui est une des quatre grandes zones dont je parlais dans la décomposition 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é, c'est presque tout le temps des enfants de Window plus spécifiques qui sont utilisés, le but est que chaque type d'information sur l'image aie son type de window.
Voilà donc un petit diagramme qui montre le découpage du programme :
Pour visualiser encore un peu mieux comment ce découpage prend forme, voici ce que chaque zone et Window contient.
Main Zone :
Driver Zone :
Driver Position Window :
Driver name Window :
Driver LapTime Window :
Driver Tyre Window :
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écifique, car la manière de reconnaitre le pneu utilisé et le temps au tour ne peut pas être la même.
Pour résumer, on a un programme qui prend en entrée un fichier de configuration, qui prend des images de la F1TV et les découpe dans des ZONES qui elles même sont découpées en WINDOWS pour qu'on puisse plus simplement les décoder.
Maintenant qu'on a une liste de différents types de zones, on peut commencer à chercher ce qu'il y a marqué 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ès simple, nous avons un modèle qui est entrainé. C'est-à-dire qu'on donne à un programme un très grand nombre de mots ou de lettres en lui disant ce que contiennent chaques images. Ensuite le programme va créer des matrices de convolutions pour chaque lettre avec comme objectif de détecter les points communs entre les lettres pour créer un alpphabet.
Par exemple, la matrice de la lettre 'H' donnerait un poids important à des lignes verticales connectées par une ligne centrale. Et si on fournit assez de données de bonne qualité au modèle, les matrices peuvent être très efficace à détecter si une lettre est un H ou un M.
Il y a pleins d'autres méthodes comme l'utilisation d'un dictionnaire de mots de la langue pour permettre la reconnaissance de mots même si une lettre au milieu n'est pas comprise ou en ajoutant d'autres informations sur le contexte, mais ça ne nous intéresse pas ici.
C'est important de comprendre comment cette reconnaissance de caractères avec des matrices fonctionne, car cela va nous aider à préparer nos données pour lui rendre la vie facile et augmenter la précision de nos résultats.
Filtres et traitement
On peut essayer de donner toutes nos images directement à Tesseract pour qu'il reconnaisse tout le texte qu'il y voit, mais on risque de se retrouver avec des résultats 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ément aisé de toujours les différencier. De plus, comme ils sont petits, les artéfacts d'aliasing sont assez violents et peuvent grandement déformer une lettre ou un chiffre.
Exemple :
Prenons le chiffre 9. Dans l'image, il peut être représenté de cette manière :
On peut voir qu'il est flou, pour nous cela ne pose pas de problème et je pense qu'à peu près n'importe qui peut dire que c'est un 9.
Cependant, comme les contours sont flous et même si on essaie de retirer le background :
On voit que le 9 n'est pas clairement défini. En effet, on pourrait le comprendre comme :
Ou comme :
Voire simplement comme :
Et on se rend bien compte que les performances de détection ne sont pas les mêmes dans ces trois cas.
Il faut donc faire un certain post traitement des images pour supprimer les éléments parasites, les couleurs, et augmenter la visibilité des contours importants.
Mais chaque type de donnée va avoir des méthodes de post traitement différents.
Donc voici les différents types de reconnaissance et leur post traitements :
Texte
Alors ce type de reconnaissance est utilisé 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èrement bien entrainé 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ée :
Ce texte peut paraitre bon, cependant quand on le lance dans Tesseract, il ne va pas toujours donner un résultat parfait. Il faut aussi savoir qu'il y a des noms pas mal plus pénibles que Tesseract à plus de mal à reconnaître, soit à cause des lettres utilisées, soit, car le nom est un nom d'une autre région et qui ne veut rien dire en anglais, ce qui empêche 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ème, 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écouvertes, voici les étapes que j'ai mis en place.
1 : J'inverse les couleurs. Je me suis rendu compte qu'il était souvent plus facile de trouver un noir sur blanc que blanc sur noir. Je ne suis pas sûr que cette étape soit capitale cependant
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.
3 : Je fais un Resize de l'image pour avoir une meilleure résolution et permettre une meilleure détection. J'augmente la hauteur et la largeur par un facteur 2. J'ai trouvé cette valeur suffisante et aller plus haut consomme beaucoup de ressources.
4 : Je fais une très rapide Dilatation du texte pour retirer le flou amené par la méthode de Resize. Je n'utilise qu'une valeur de 1, car je ne veux pas trop changer comment le texte est modelé, je veux juste retirer le flou.
Explication des méthodes précises plus bas
Voilà pour ce qui est du post processing. Je ne dis pas que ce sont les meilleurs paramètres possibles, mais dans mes tests ce sont ceux qui ont le mieux marchés.
Ce sont aussi les premières méthodes que j'ai pu développer alors forcément, elles n'ont pas le niveau de détails de certaines autres.
Mais comme même avec ce traitement, il n'est pas rare que je me retrouve avec une ou deux lettres pas justes, il faut un moyen d'être sûr que c'est le bon nom qui est trouvé. Ce qu'il y a de pratique avec les noms de pilotes, c'est qu'on sait déjà 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 à trouver parfaitement les bonnes lettres, on peut simplement essayer de trouver quel nom de pilote ressemble le plus au nom trouvé sur l'image.
Pour ce faire, j'ai utilisé une méthode appelée la distance de Levenshtein. Pour faire simple, c'est une méthode qui va calculer les distances de lettres pour déterminer entre des strings laquelle ressemble le plus à une autre.
Pour résumer le fonctionnement dans l'ordre :
- On prend l'image, on la traite
- On envoie l'image traitée à Tesseract
- On trouve quel nom de pilote ressemble le plus à ce résultat
- On renvoie le nom du pilote
Chiffres
Cette méthode en réalité utilise juste la même méthode que celle qui va récupérer le texte sur une image. Cependant, là, on envoie à Tesseract l'information qu'il ne peut trouver que des chiffres sur l'image, ce qui lui permet d'être beaucoup plus précis et de ne pas confondre un 9 avec un P ou un 11 avec un H PAR EXEMPLE (non pas que ça me soit arrivé très régulièrement et que ça me soit resté dans la gorge évidemment).
L'avantage, c'est que cette méthode ne demande même pas de traitement de la donnée en sortie de Tesseract. On espère simplement que le post traitement aura suffit.
TEMPS :
Cette méthode regroupe la détection de temps au tour. Il y a trois grands types de WINDOW qui sont concernées :
- La WINDOW du temps au tour
- La WINDOW du retard sur le leader
- La WINDOW des secteurs
La grande différence ce sont les ordres de grandeur. Les temps au tour sont en général entre 50 secondes et deux minutes. Tandis que les secteurs sont entre 20 et 30 secondes alors que le retard sur le leader peut être de plusieurs minutes.
Cependant, tous ces temps possèdent le même type de post-traitement avant d'être envoyés à Tesseract.
Voici un exemple de temps au tour avant toute transformation :
On peut avoir l'impression que ce texte est tout à fait lisible et facile à décoder, surtout quand on le voit de loin comme ça. Cependant, il faut imaginer que ces chiffres font 13 pixels de haut en comptant le flou et comme expliqué plus haut, ce flou dans ces échelles est terrible.
Si on donne cette image à 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ère complètement imprévisible. Ça n'est simplement pas utilisable.
Cette partie est un peu plus complexe, car si la détection n'est pas fiable, les chiffres sont juste inutilisables. Si à tout moment un temps au tour de 1:39.106 devient 1:32.108 c'est juste pas possible.
Voici donc les étapes de post-traitement que j'ai mis en place pour leur détection :
1 : J'applique un Treshold de 185 pour enlever les ambiguïtés d'alisaising et avoir une image en noir et blanc claire. La valeur de 185 est assez élevée, 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écifiques. Je me suis rendu compte que cette valeur était une de celles qui marchent le mieux.
2 : J'applique un Resize de 2 pour augmenter la résolution des chiffres et permettre une meilleure détection. Le but est d'avoir plus de pixels et donc de permettre à Tesseract de mieux utiliser ses matrices de convolution.
3 : Comme le Resize amène du flou, j'utilise une méthode de Dilatation qui me permet de retirer ce flou et de remplir un peu plus certaines parties qui ont été un peu laissée par le Resize;
4 : Contrairement aux mots plus haut, la rondeur ajoutée par la dilatation n'est pas vraiment désirée. En effet, elle peut rendre confuse certains chiffres et empêcher Tesseract de bien trouver le chiffre. Alors, j'applique une Érosion qui me permet de contrecarrer en partie les rondeurs ajoutées par la dilatation et retrouver des chiffres bien formés. Pour l'Érosion et la Dilatation, j'ai utilisé une valeur de 1, car je ne voulais pas trop changer les chiffres.
Explication des méthodes précises plus bas
Et avec ce post processing, on retrouve de plutôt bons résultats qui demandent peu de traitement.
Le traitement dépend du type de WINDOW cependant :
- Pour les secteurs, on indique à Tesseract que les caractères autorisés sont : "0123456789."
- Pour les temps au tour, on autorise plutôt "0123456789.:"
- Et pour les écarts, on autorise "0123456789.+"
Ensuite, on récupère 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 à détecter quand ça arriver.
Je passe les détails 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ésumer le fonctionnement dans l'ordre :
- On prend l'image et on lui applique une série de filtres
- On envoie l'image filtrée à Tesseract
- On nettoie le résultat Tesseract pour compenser certains biais
- On convertit le résultat en millisecondes
les chiffres (2)
Il faut savoir qu'avec la dernière version de l'émulateur (dont je vais parler un peu plus tard).
Pneus
Là, on arrive sur la partie la plus pénible.
Pour comprendre la problématique, 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édiaires
- Les pneus pluie
Les trois premiers pneus sont des pneus faits pour piste sèche, le pneu intermédiaire pour piste humide et le pneu pluie pour la pluie.
Chaque pneu a sa durée de vie et son niveau de performance propre, mais je ne vais pas rentrer dans le détail 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ès importante.
Chaque pneu a une couleur donnée qui permet de les différencier.
Voici un exemple de ce à quoi une WINDOW de pneus peut ressembler :
Mais cette zone peut aussi ressembler à ça :
Mais aussi à ça :
Voire même ça :
Je pense que vous pouvez tout de suite comprendre la difficulté que représente la tâche de récupération de données à partir de cette image.
En gros, le fonctionnement de cette zone d'information est assez simple.
- Au fur et à mesure que la course avance, le trait fait de même.
- Le chiffre dans le round tout à droite indique le nombre de tours que le pilote a passé sur ce pneu.
- La couleur indique le type de pneu.
- S'il y a une lettre à 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 à Tesseract, on ne récupère ni les chiffres ni les lettres correctement si ce n'est pas du tout.
Il faut donc utiliser une méthode qui permette d'isoler le rond le plus à droite, lui appliquer un traitement qui permette à Tesseract de lire ce qu'il y a marqué et qui puisse déterminer quel pneu est en train d'être utilisé.
J'ai décidé 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'à trouver un obstacle. Je détecte un obstacle si le pixel sur lequel est mon trait possède une valeur de plus de 0x50 dans le channel R, G ou B. J'ai trouvé en faisant des tests que les couleurs de background de la F1TV ne dépassaient jamais ces valeurs.
Ensuite, après avoir trouvé le premier obstacle, je récupère une zone qui doit englober le cercle.
Voici un exemple avec cette image en entrée :
Elle est automatiquement coupée de cette façon :
Cela me permet d'isoler uniquement ce qui m'intéresse, ce qui est très pratique pour Tesseract et pour la détection 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ûrement partie du background ou du chiffre.
Ensuite, j'utilise une méthode qui calcule la différence entre la couleur obtenue et la liste de couleurs possible.
Il y a cinq couleurs des pneus possibles :
"#ff0000" pneu tendre/soft
"#f5bf00" pneu medium
"#a4a5a8" pneu dur/hard
"#00a42e" pneu inter
"#2760a6" pneu pluie/wet
Ce qui est pratique, c'est que même dans les cas où il n'y a pas beaucoup de couleur comme celui-là :
On arrive à une couleur moyenne de :
Et il est donc assez facile de déterminer le type de pneu en question.
Attention, les résultats peuvent être très vite dérangés par la couleur du pneu précédent si le découpage de la fenêtre n'a pas été assez précis.
Ensuite il "suffit" de lire le chiffre dans le rond et si on n'arrive pas à 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ès sympathique de la lecture du chiffre.
Vous saurez que Tesseract, en plus de détester les grandes images et les images avec des couleurs, déteste également les formes dans une image. Ainsi dans notre cas, le round de couleur autour du chiffre, même s'il n'est pas complet, il interfère avec la reconnaissance et empêche 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é des traits depuis les bords de l'image jusqu'à 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 :
Ensuite, on peut retirer les pixels qui ont une valeur dans un channel RGB plus haute qu'un certain seuil :
Et là, on a ce que l'on veut !
À partir de là, ce sont les filtres que l'on connait qui sont utilisés pour en faire une image plus facile à utiliser par Tesseract.
1 : On effectue un Resize de facteur 4 (oui, c'est beaucoup, mais en même temps le chiffre est vraiment petit à la base) qui permet d'avoir une image d'une bien meilleure résolution.
2 : On fait une Dilatation de facteur 1 pour retirer tout le flou de l'image pour aider Tesseract
Et on a un chiffre qui est utilisable par Tesseract !
Explication des méthodes précises plus bas
Pour résumer :
- On prend l'image de la zone et on la crop pour ne garder que la partie essentielle
- On détermine 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ésolution du chiffre
- On rend ce chiffre net
- On envoie l'image traitée et filtrée à Tesseract
- On détermine le nombre de tours que le pilote a fait avec ses pneus avec le résultat de Tesseract
DRS
Bon ça, c'était plutôt simple, j'ai simplement vérifié si la moyenne de vert dépassait une certaine valeur et puis voila.
Filtres et méthodes sur les images
Dans ce projet, on a dû utiliser différentes méthodes d'édition d'image, que ce soit sous forme de filtres ou de modification de l'image directement. Voici un sommaire des méthodes utilisées et comment elles fonctionnent.
Tresholding
Cette méthode sert à passer d'une image en couleurs à une image binaire noir-blanc. C'est une étape très importante pour l'OCR car elle permet (si bien faite) d'isoler du texte de son background.
Un exemple ici :
Le fonctionnement est assez simple, mais il peut être fait de différentes manières, mais dans mon cas voici comment l'algorithme fonctionne sachant qu'il demande en entrée 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ême valeur en R,G et B (Formule utilisée : gray = R x 0.3 + G x 0.59 + B x 0.11)
- Si le résultat de la valeur de gris est au-dessus de la valeur de treshold, le pixel est passé en blanc complet et dans le cas contraire, il est passé en noir complet.
- On retourne la Bitmap modifiée
Un algorithme pas forcément complexe, mais qui peut augmenter de manière titanesque les chances de réussir une OCR
Resize
Cette méthode sert à augmenter la résolution d'une image pour améliorer la précision 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éthode d'augmentation de la taille avec une simple interpolation. En effet, une augmentation de taille interpolée ne va pas vraiment changer la résolution, l'image sera toujours aussi pixelisée, seulement, les pixels seront composés de plus de pixels comme dans l'exemple ci-dessous :
Dans mon projet, j'utilise de l'interpolation bicubique qui va créer de l'information pour tenter de combler le vide et produire une image réellement plus grande et avec plus de détails, mais en ajoutant du flou.
Le but est d'aller chercher dans les pixels alentours les couleurs qui sont déjà présentes et de jouer avec des poids pour tenter de faire une prédiction de ce que ce pixel aurait été si l'image avait plus de détails.
Voici un exemple assez parlant :
On pourrait croire que c'est inutile, mais dans le contexte de Tesseract ajouter des détails pour tenter de simuler une meilleure résolution même en créant du flou est intéressant pour mieux remplir la matrice de convolution.
Mais il est possible de réduire ce flou avec d'autres méthodes également.
(Dans mon code, je n'ai pas utilisé 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édé assez lourd en performances.
Dilatation et Érosion
Cette méthode et la suivante font partie des méthodes de transformation morphologiques.
Ces méthodes sont utilisées pour accentuer les formes et les épaissir ou les réduire et les affiner. Elles possèdent l'aventage aussi de retirer le flou d'une image ce qui est très pratique si utilisé après l'utilisation de méthodes comme Resize.
Je ne vais pas trop rentrer dans les détails de ces méthodes, car leur fonctionnement est un peu plus lourd en math si on veut faire une véritable explication du pourquoi et du comment ça marche aussi bien. Pour notre projet, je dirais que l'important est de savoir que ce sont deux outils très pratiques pour changer la morphologie des lettres et des chiffres et qu'on peut les utiliser pour corriger du flou et/ou des artéfacts apparus lors de la binarisation de l'image ou de la suppression de fond.
Remove Background
Cette méthode est assez simple et est juste une méthode qui va passer en revue tous les pixels de l'image et si la couleur d'un pixel s'apparente à celle d'un pixel de fond, il est passé en noir total ou en blanc total. Le but est de permettre au reste du programme de fonctionner avec des couleurs moins ambiguës.
Une variante spécialisée pour la reconnaissance des pneus appelée affectueusement Remove Useless cherche à atteindre le même bu, mais est bien plus sophistiquée et spécialisée 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étails, voir la détection de pneus.
Il y a aussi d'autres méthodes comme un filtre Gaussien ou Highlight countour que j'ai dû développer, mais que je n'ai pas utilisé donc je ne vais pas en parler ici.
Petit point résolution
Comme on peut l'imaginer, la résolution est extrêmement importante pour l'OCR. Et en avançant sur le projet de l'émulateur, je me suis rendu compte qu'il était possible de récupérer des images en 4K (Plutôt 1080 avec l'upscaling du lecteur). Cela est une superbe nouvelle car cela permet de simplifier énormément le processing sur les différentes windows.
Quelques exemples pour se faire une idée
Mais il faut savoir que grâce à cette simplification, j'ai pu aussi créer d'autres méthodes de filtrage pour certaines parties. Mais la simplification était obligatoire, car avec des images aussi grandes, il n'était simplement pas possible de venir appliquer les mêmes filtres car le temps de traitement serait beaucoup plus long.
J'indique ces changements que après l'explication d'avant car ce sont des changements un peu de dernière minute et que la logique expliquée plus haut a été très importante pour le projet OCR même si tout n'est plus forcément utilisé maintenant que j'ai des images de meilleure qualité.
Dans la version actuellement disponible, la reconnaissance a été simplifiée sous cette forme :
- Le "GapToLeader" est décodé avec un premier passage de Tresholding à 165 puis un Resize de 2 et une Dilatation de 1 pour retirer le flou
- Les "Sectors" sont décodés en utilisant une toute nouvelle méthode VanishOxyAction à cause des couleurs parfois appliquées et ensuite simplement une methode de Tresholding de 150 pour rendre le résultat assez propre pour l'OCR.
- Le "LapTime" est d'abord passé par un Tresholding très strict de 185 pour préparer la SobelEdgeDetection qui est également une nouvelle méthode qu'il a été possible d'utiliser grâce à la simplification du reste des processus.
- Le "Text" est décodé juste avec un tresholding de 165 désormais grâce à l'image 4K.
- Les pneus ont leur propre traitement comme expliqué 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éthodes que je n'utilise plus ne sont pas utiles. La reconnaissance n'est pas encore parfaite et je pense que leur utilisation pourrait aider à améliorer les résultats. (Et parfois ces anciennes méthodes sont utiles dans les traitements personnalisés des Windows elle même comme par exemple les pneus qui utilisent la méthode GrayScale pour isoler les couleurs)
VanishOxyAction
Cette méthode est une méthode plutôt simple, mais qui est importante. Elle se base beaucoup sur le code de la méthode Grayscale et sur la méthode Tresholding car elle essaie de regrouper le meilleur dès deux en réglant quelques soucis que ces dernières créaient.
Les soucis avec la méthode grayscale c'est que quand le texte est de couleur (Ce qui arrive souvent pour les temps de secteurs) la méthode GrayScale rend les couleurs dans une nuance de gris un peu trop sombre ce qui fait qu'ensuite la méthode de Tresholding défonce tout.
L'idée 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ême niveau pour avoir une image blanchie qui puisse être ensuite utilisée avec la méthode de Tresholding sans soucis.
On pourrait se dire qu'avec ce genre de méthode le tresholding est inutile ensuite, mais ça n'est pas le cas, car le tresholding sert ensuite pour rendre les contours plus ou moins agressif. Parce que même si l'image ressemble à une image binarisée, il reste des nuances que le treshold va pouvoir utiliser.
SobelEdgeDetection
Cette méthode est une méthode assez classique que je n'ai pas designé moi-même alors, je ne vais pas trop m'épancher dessus.
En gros, on utilise une matrice et une formule mathématique pour redessiner une image et le résultat est une image avec des contours. Je ne l'ai utilisé que pour les temps au tour, car ce sont les plus récalcitrants.
Cette méthode a besoin d'une image passée en noir et blanc au préalable à laquelle on applique ensuite les matrices de filtres. Et avec ces filtres ajoutés à l'image, on peut ensuite calculer le "Gradient" pour créer les bords.
Le seul souci de cette méthode, c'est qu'elle est assez gourmande et qu'elle fournit des formes creuses dû à la nature des matrices données.
Voici un exemple de ce dont cette méthode est capable :
Apparemment l'OCR aime assez bien cette méthode et elle permet de beaucoup moins souvent oublier les '.' ou ':'
Traitement des données
C'est bien gentil de recevoir des résultats de l'OCR, cependant on ne peut pas souvent les utiliser comme tels. En effet, les résultats ne sont pas très constants et demandent d'être vérifiés pour savoir s'ils doivent être pris en compte.
Le post traitement de ces données dépend complètement du contexte et donc il est différent pour chaque type de window.
Voici un florilège des différents types de traitements :
- Traitement du nom de pilote
Rien de plus que ce qui est déjà détaillé dans la partie OCR
- Traitement des pneus
Pareil
- Traitement des temps
Là, par contre, c'est intéressant. Dans un monde parfait, je pourrais simplement prendre les résultats 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ées, on ne peut pas.
Le problème vient du fait que les temps que l'on peut trouver sur la F1TV sont encodés avec des '.' et des ':' qui déterminent les limites entre les chiffres qui désignent les minutes, les secondes et les millisecondes. Et le souci avec ces séparateurs, 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és ou pris en double, c'est un enfer.
Il faut donc trouver un moyen de détecter quand cela arrive. Et je n'ai pas trouvé de meilleurs moyens que de faire du cas par cas.
Cela peut paraître simple quand on parle par exemple des secteurs. On sait qu'on attend deux chiffres avant un '.' et trois chiffres après. Il est ainsi facile de voir que si je trouve six chiffres et pas de séparation, le troisième est le séparateur mal compris.
Mais l'exemple qui détruit vraiment tout, ce sont les écarts 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 écart avec le leader ça peut être 0.345 comme 1:12.345. Ce qui fait que lorsque je reçois 121345 est-ce que c'est 12.345 ou 1:21.345...?
Souvent, on peut quand même déduire, mais cela demande de prévoir presque tous les cas limites, ce qui est assez pénible.
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 écart prenne d'un coup une grosse différence. Cela arrive même assez souvent quand des pilotes sortent de la piste.
Pour ce qui est du DRS et de la position des pilotes, il n'y a pas vraiment de traitement supplémentaire. Non pas, car la détection est parfaite, mais par ce que la détection ne peut pas rater de 200 façons. 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à de l'OCR
Stockage des données
Dans ce projet, le but n'est pas simplement de trouver les données et les afficher. L'intérêt de les récupérer est de pouvoir les comparer à d'autres données précédentes.
Le vrai souci de la F1TV c'est justement que l'on ne peut pas facilement voir les évolutions. On ne peut voir que des "photos" de la situation actuelle de la course.
Il faut donc garder en mémoire les différentes choses qui se sont passées. Techniquement, on pourrait stocker ces données dans de bêtes listes C#. Mais le souci avec ça, c'est que même si des outils comme LinQ existent, ça 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-être pu se satisfaire de listes simples, le but est d'ensuite pouvoir construire sur ces bases pour faire des prédictions et des insertions de stats beaucoup plus intéressantes qui demandent de faire des requêtes complexe rapidement.
Je me suis dit que la meilleure méthode serait d'avoir une base de donnée dans laquelle je peux faire des requêtes SQL. Mais, comme je n'ai pas besoin de toutes les features de SQl et que je ne veux pas avoir à gérer un serveur de base de donnée et tout ce qui va avec, je me suis dit qu'une bonne option serait d'utiliser 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é et d'utilisation de requêtes SQL.
J'ai créé trois tables dans cette base de donnée SQLite que voici :
Base de donnée
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 été effectué | PRIMARY |
| DriverID | INTEGER | Pilote qui a effectué le Pitstop | PRIMARY |
| Tyre | VARCHAR | Pneu chaussé par le pilote | NOT NULL |
Stats
| Colonne | Type de Data | Description | Tag |
|---|---|---|---|
| Lap | INTEGER | Tour durant lequel le Pitstop a été effectué | PRIMARY |
| DriverID | INTEGER | Pilote qui concerné | PRIMARY |
| Tyre | VARCHAR | Pneu chaussé 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 à stocker les différents noms de pilote pour qu'ils soient utilisés dans le reste de la DB
La table Pitstops n'est pas vraiment utilisée dans l'état actuel du projet. Mais le but était de la remplir dès que le programme détectait un arrêt aux stands. Le but est ensuite de pouvoir construire un classement pondéré en fonction des arrêts des différents pilotes et d'afficher la stats tout le temps sur l'affichage principal. Elle n'est pas réellement utilisée, car la détection de pitstop n'a pas pu être complétée. De par la nature des données récupérées des pneus et des positions, c'est très difficile de détecter avec précision un arrêt aux stands.
La table Stats est la plus importante parce qu'elle contient toutes les informations concernant les pilotes à chaque tour. L'idée est qu'elle soit remplie à chaque tour. Les infos ne sont pas censées être les infos lives, mais plutôt juste une photo à chaque tour de la situation de chaque pilote pour ensuite pouvoir faire des comparaisons tours par tours. Des données comme le GapToLeader peuvent évoluer 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és dans des listes et les informations long terme qui sont stockées dans la DB.
À chaque itération de l'OCR, les données récupérées sont stockées dans une liste de DRIVERDATA.
Les DRIVERDATA sont des structures de données qui contiennent toutes les infos d'un pilote à un instant T. Elles peuvent être incomplètes et sont juste là pour faire de petits calculs et déterminer quand insérer des données permanentes.
Ce qui nous amène au moment intéressant. Comment on détermine quand il est intéressant d'insérer des informations dans la base de données.
Il y a deux cas de figure ou on pourrait vouloir insérer des infos :
Quand un pilote a fini un tour
En effet, j'ai estimé que les seuls moments où on veut garder une photo de la situation du pilote, c'est, car il passe d'un tour à l'autre. Le raisonnement est le suivant : On ne veut pas conserver TOUTES les données parce que si on prend une photo toutes les trois secondes, la majorité des informations seront redondantes avec les précédentes. Mais en même temps, il ne faut pas rater des changements importants de données. Les seules données qui changent entre deux passages de l'OCR sont les écarts entre les pilotes et de temps en temps un nouveau secteur s'affiche. Alors que d'un tour à l'autre presque toutes les informations changent. Et on ne perd que les légères fluctuations des écarts entre les pilotes.
J'ai donc décidé de conserver une photo par tour. Mais c'est bien joli sauf qu'il reste une difficulté : Comment savoir qu'un pilote a fait son tour ?
Cela peut paraître simple comme question, mais elle est plus difficile qu'il n'y parait. Il faut savoir qu'en F1 un pilote peut être dans son 26ᵉ tour pendant qu'un autre en est à son 24ᵉ. Chaque pilote a sa propre course et au fur et à mesure que les écarts se creusent, il peut y avoir un tour voir plusieurs d'écart entre la queue de course et les premiers pilotes.
Ensuite, il faut savoir qu'il n'est pas marqué sur la f1TV dans quel tour chaque pilote est. Il faut donc le déduire en fonction des Data.
Voici le code le if qui détecte 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ésente les 20 photos des données des pilotes.
Cela veut dire que DriverDataLogs[3] représente toutes les infos des pilotes dans le tour 4 et que DriverDataLogs[3][0] représente toutes les infos du premier pilote dans le tour 3.
Si on analyse un peu ce qui est écrit avec ces informations, on peut voir que je détermine qu'un nouveau tour se définis comme une photo ou le troisième secteur a été complêté et ou il ne l'était pas juste avant. Cela fait sens car quand un pilote complête son troisième secteur c'est la que son dernier temps au tour se met à jour.
Le reste des tests est juste la pour éviter 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ès spécifiques 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êt aux stands
Et la on touche le plus difficile. Pourtant un arrêt aau stand ne devrait pas être compliqué à detecter. C'est quand un pilote change de pneu. Alors il peut changer de pneu en gardant le même type de pneu et donc tout repose sur le nombre de tour qu'un pneu fait.
Sauf que il faut ajouter à cette reflexion qu'un pneu peut être chaussé sans qu'il soit neuf. Ce qui veut dire que l'on ne peut pas simplement choisir qu'un pilote a changé de pneus quand ses pneus sont à 1 tour. Il peut très 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énible qui fait que les deux premiers tours faits avec ne sont pass vraiment déchiffrables car ils sont un peu cachés derrière la lettre qui indique le nouveau pneu chaussé.
Exemple :
Voici le code que j'avais écrit 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 à 0 tours (ce qui est le moment ou il y a une lettre à la place d'un numéro de tour) et que la photo d'avant montrait un pneu normal.
On vérifie aussi que le pneu a bien été detecté 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 à cette metric.
Si on veut utiliser cette methode pour trouver les Pitstop il va falloir avant tout améliorer l'OCR sur ce point.
Ce soucis mets en lumière un principe assez important de l'informatique "Ggarbage in, Garbage out". Si les données que je recoit ne sont pas géniales, le résultat ne sera pas génial 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ère 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ées
Maintenant que l'on a stocké toutes ces données, il faut en faire quelque chose sinon ca ne sert a rien.
Afficher les données est techniquement la partie la plus simple du projet. Il faut prendre les données qui nous intéressent de la base de données et des résultats 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évus initialement :
Affichage direct
L'affichage direct est simplement l'affichage du résultat de l'OCR.
Par exemple le classement live ainsi que les écarts entre les pilotes sont affichés directement depuis les résultats de l'OCR. Ce ne sont pas forcément des données prises dans la base de donnée.
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 à des erreurs. En effet, si un pilote est mal detecté on le verra directement mal s'afficher dans la Form principale.
C'est également l'affichage le moins intéressant car il ne crée aucune information, il ne fait que remontrer les infos que l'on peut déja voir dans la F1TV
Affichage calculé
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ée. Ils ne font pas de calculs à proprement parler mais ils affichent plus d'informations que ce que montre la F1TV. Cela veut dire qu'ils représentent un début de plusvalue par rapport à l'alternative qu'est la page DATA de la F1TV.
Ils ne sont pas beaucoup plus durs à implémenter mais demandent de faire des requêtes à la base de donnée.
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 à moins d'avoir une excellente mémoire.
Voici un exemmple du type de code necessaire pour afficher ce genre de données:
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é ici n'est pas forcément le code utilisé dans le projet.
D'une certaine facon les fenêtres de bataille et de dépassements sont aussi des hybrides.
Ici ce sont les batailles qui sont représentées. Aucune donnée n'est calculée, c'est litterallement directement les données 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 à 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èrés comme êtant en train de se battre sont les pilotes à 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
La c'est l'historique des dépassements qui est affiché. On pourrait presque dire que c'est un affichage complêtement calculé 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érences entre l'ancienne position d'un pilote et la nouvelle et on affiche les changements.
Affichage totalement calculé
L'affichage complêtement calculé est un type d'affichage qui ne montre aucune information trouvée sur la page de la F1TV. C'est le premier affichage à traiter l'information qu'il trouve et il retourne des informations nouvelles. La nuance avec les affichages prédictif est qu'il ne crée pas réellement de l'information, il la déduit.
Le but est de prendre un certain nombre d'informations trouvées sur la page de la F1TV et de calculer des choses pour faire ressortir des tendances à l'utilisateur. Cependant on reste sur des informations factuelles. Ce sont des infos déduites que techniquement unn humain avec une bonne mémoire et fort en calcul mental pourrait faire. Mais la c'est fait automatiquement pour tous les pilotes et c'est affiché de sorte à faire ressortir les valeurs spéciales.
Comme c'est un peu plus abstrait, je pense qu'un exemple vaut mieux que 1000 mots.
Ci dessus on peut voir un bon exemple. C'est une fenêtre qui montre qui sont les pilotes les plus rapides et les moins rapides et qui montre la différence de temps au tour.
Cette information est totalement déduite et n'est en aucun cas trouvable sur la F1TV mais elle n'est pas inventée. Elle est simplement calculée.
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'écart.
C'est une stat assez intéressante car elle lisse les différences d'un tour à 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éressants que les pneus secs car on voit que les pilotes les plus rapides sont les pilotes de fond de grille qui ont chaussé 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édictif
C'est ici que ca devient vraiment dommage, le projet a mannqué de temps pour implémenter des affichages prédictifs mais le potentiel est la !
Un affichage prédictif est un affichage qui crée des informations à 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édictifs qui pourraient être 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-être devoir s'arrêter.
- Si un pilote tourne une seconde au tour plus vite que le pilote devant lui et que ce pilote est à 10 secondes devant, alors il devrait pouvoir le rattraper d'ici dix tours.
- Si un arrêt au stand est en moyenne de 23 secondes, alors un pilote 3ème ressortirais potentiellement 7ème si il s'arrête maintenant.
Tous ces exemples sont des mini algorythmes prédictifs qui pourraient être implémentés assez facilement dans l'architecture actuelle du projet et pourraient apporter une immense plus-value si ils sont bien paramêtrés.
On peut même 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ées sont infinies !
Tests
Alors la on arrive à la GROSSE erreur de ce projet...
Si je ne pouvais changer qu'une seule chose à ma facon de faire le projet après 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érifier que du nouveau code est performant et est beaucoup plus pratique à utiliser.
Je pense sans rire que j'aurais pu gagner plusieurs jours de travail si j'avais travaillé différemment vis-a-vis des tests.
Comment ca c'est passé
Dès la création du planning prévisionnel j'ai fait une erreur capitale. J'ai mis les tests en fin de developpement des features... Et je ne leur ai laissé que très peu de temps tout en les mettant au milieu du chemin critique ce qui les rends particulièrement vulnérables si une tâche du chemin critique est retardée.
En fait dans ce projet je voulais surtout éviter de faire comme certains projets que l'on a pu avoir pendant notre formation. C'est à dire que je ne voulais surtout pas oublier la doc. Alors j'ai agencé le projet pour commencer par les fondations de la doc, puis en incluant les périodes de programmation et entre ces dernière ajouter des Tests dans les trous. Le soucis c'est que du coup les projets étaient un peu le dernier truc dont je devais me soucier ce qui a été une très mauvaise idée.
Je me suis retrouvé à devoir mordre sur les jours de tests car les tâches de programmation mettaient plus de temps que prévu (qui elles-même auraient pû être plus courte avec une bonne utilisation des tests) et je mme suis retrouvé à passer outre les tests pour avancer sur le reste du projet.
J'ai donc du en panique à la toute fin du projet construire quelques tests "unitaires" dont l'utilité est très limitée car tout le travail a déja été fait et que vu la complexité qu'a pris le projet, faire de vrais tests unitaires est devenu un peu trop compliqué pour valoir le coup.
Les seuls tests "unitaires" (Je l'écris entre quotes car ce ne sont pas vraiment des tests unitaires mais plutôt des tests tout courts car ils ne sont pas spécifiques) qu'il y a dans le projet final sont des tests exclusivement tournés sur l'OCR. Ils sont déja vraiment pratiques car cela me permet de tester d'autres algorythmes d'OCR et voir si les résultats sont meilleurs ou non mais c'est juste un peu trop tard quoi...
Les tests unitaires que j'ai implémentés sont un peu tous pareils au niveau du fonctionnement :
- On choisit une image dans une liste d'images préparées qui sont scensée représenter le type de données rencontrées par l'application en temps normal
- On lis le nom de l'image que j'ai mis manuellement en indiquant ce qui était marqué sur l'image
- On fait un coup d'OCR sur l'image et on compare ce résultat avec la valeur que l'on est scensé 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émentaire qui ne fait pas partie des étapes citées est juste la pour manipuler le format des résultats pour qu'il soit comparable.
Ce qui est pratique avec cette approche, c'est qu'il est très facile de rajouter des cas spécifiques et voir comment le programme les gère. Si je vois qu'un certain nombre est souvent mal reconnu, je peux faire exprès de le mettre dans le dossier et modifier mon code d'OCR jusqu'à ce que le test passe.
Si j'avais eu plus de temps, j'aurais sûrement pu ajouter de vrais tests unitaires qui testent des fonctions très précises. Par exemple, vérifier que les différents Windows sont bien appelées et que les zones se créent correctement ou même plus simplement que la lecture du JSON au démarrage marche bien.
Il faut savoir que même si je n'ai pas eu l'occasion d'écrire beaucoup de tests sous forme de code. Toute la phase de développement de l'OCR, j'ai passé plus d'une heure par jour à analyser les résultats. 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ésultats pour isoler ceux qui posaient un problème.
C'est comme ça que je me suis rendu compte par exemple que, avec cette police, les quatre et les 1 étaient souvent confondus. Donc même si les tests automatisés sont clairement insuffisants par rapport à ce que j'aurais peut-être dû faire, j'ai passé énormément de temps à tester mon application.
Comment ç'aurait dû se passer
Si je devais refaire ce projet aujourd'hui, je pense que j'utiliserais un peu la même technique que pour la doc. J'aurais mis les tâches de Tests directement au début du projet et j'aurais déterminé le squelette de l'application par la même occasion.
Je pense que j'aurais mis trois jours pour écrire tous les tests dont j'aurais besoin et j'aurais fait une stratégie de TDD (Test Driven Developpement) par ce que je pense que ça marcherait vraiment super bien sur ce type de projet.
J'aurais pris, je pense, cinq une dizaine d'images complètes de la F1TV de plusieurs GP différents et j'aurais mis toutes les fenêtres découpées dans des fichiers avec des tests comme ceux que j'ai faits pour ce projet. Et comme ça je saurai que mon algo est bon uniquement quand il aura réussi à passer tous les tests.
Cela règlerait le souci que j'ai eu le plus : Me retrouver à devoir changer l'OCR 5 fois par ce qu'à chaque fois que je développe une nouvelle feature, je me rends compte d'une faiblesse, mon algorithme…
Non seulement j'aurais eu beaucoup plus de facilité à avancer sur le projet, mais en plus, je pense que cela m'aurait fait gagner énormément de temps non seulement, car je n'ai plus à tester tout à la main, mais en plus par ce que ça veut dire que quand l'OCR passe les tests, je n'ai plus jamais à m'en soucier.
Leçons
Je pense que dans mes futurs projets, je mettrai les tests en début de projet plutôt qu'à la fin et je ferai en sorte qu'ils fassent partie du chemin critique et que je ne puisse pas passer à côté sous prétexte que "Je n'ai pas le temps".
Écrire des tests, ce n'est jamais marrant et c'est encore moins marrant quand ils nous empêchent d'avancer. Mais je suis convaincu qu'à la fin, c'est un gain de temps et de sérénité incontournable.
Résumé des difficultés techniques
Ici, je vais parler très rapidement des difficultés techniques rencontrées. Si vous voulez tout savoir à propos des difficultés, vous pouvez aller lire le journal de bord. C'est aussi pour éviter de me répéter par rapport aux explications des différents points dans l'analyse organique.
Je ne vais pas non plus parler des difficultés rencontré avec des choses que je n'ai pas gardées dans le programme final donc il est normal que vous vous disiez qu'il n'y a pas eu tant de difficultés que ça.
Browser Headless
Il y avait plusieurs difficultés techniques avec cette histoire de Browser Headless.
Déjà pouvoir lancer un browser headless et le contrôler. C'est difficile, car il faut trouver la bonne librairie et ensuite, il faut trouver le bon exécutable de gecko Driver qui permette de faire fonctionner l'application même si l'utilisateur n'a pas Firefox sur sa machine.
Ensuite, la seconde difficulté est celle de ne pas se faire chopper comme un bot par le site de la F1TV. Il faut savoir qu'à ce jour, je n'ai toujours pas réussi à faire croire à la page de login de la F1TV que j'étais un user normal en utilisant Sélénium mais au moins maintenant, je peux accéder aux vidéos tranquillement.
Ce souci de ne pas pouvoir se connecter avec la page de login à la plus grosse difficulté technique de cette partie du projet : la connexion automatique. Pour me connecter à la F1TV avec un browser headless la seule solution que j'ai trouvée a été d'utiliser des cookies. Et pour que l'utilisateur n'ait pas à 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é, comme on travaille avec un site web que l'on ne contrôle pas, il faut trouver un moyen de gérer les erreurs et de réessayer parfois et attendre quand il faut dans les cas où le chargement est long etc...
Ensuite, après tout ça, la dernière difficulté a été de pouvoir contrôler 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é que c'était de mettre le browser en 4K pour des raisons de santé mentale)
OCR
Les difficultés ici sont dans un autre niveau. Chaque type de donnée représentait sa difficulté à lui tout seul, sans compter l'optimisation.
Pour commencer, on a le texte pour les noms de pilotes. Il a fallu trouver un système qui puisse reconnaitre le texte et qui puisse comparer le résultat avec les pilotes que l'on connait.
Ensuite, il a fallu trouver un moyen de détecter la différence entre les fenêtres de DRS où il est ouvert ou fermé. Il fallait également faire attention à ne pas faire de faux positifs.
Pour les temps par secteurs, il a fallu trouver des filtres qui permettent de bien différencier 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ù le texte serait en couleur ça fonctionne quand même. (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éressante. 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 été 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étecter quand inévitablement cela arrive quand même.
Et la dernière difficulté (la plus pénible) a été de détecter les écarts entre les pilotes. Il a fallu trouver une façon de décoder le texte en temps, mais aussi de faire tout un système qui détecte et règle les cas ou un ':' a été oublié ou confondu tout en ne sachant pas s'il était censé y en avoir à la base, car les valeurs peuvent varier entre '1_23.657' et '0.452'.
Stockage
Pour ce qui est du stockage, la grande difficulté a été de savoir quand un pilote avait fini un tour parce que chaque pilote finit son tour à un moment différent. Il a également fallu trouver un moyen de savoir les données d'un pilote étaient logiques.
Une difficulté qui n'a pas été complètement dépassée est de savoir quand un pilote a fait un arrêt aux stands, car la détection de l'âge des pneus est plus que mauvaise.
Voilà. Ce fut une petite liste non exhaustive de quelques difficultés techniques que j'ai rencontrées pendant ce projet.
Optimisation du programme
Ici, je vais parler des techniques que j'ai utilisées pour réduire le temps de traitement de chaque image de 50 secondes à un peu moins de 3 sur le processeur de mon laptop. En effet, dans les premières versions du projet, traiter l'intégralité d'une image pouvait prendre presque une minute.
Ce qui est compliqué dans ce projet, c'est qu'il y a un certain nombre de choses que je ne contrôle pas. En utilisant Tesseract, je me retrouve avec des incompressibles. En imaginant que l'OCR sur une image prenne 300 ms, même si j'avais 180 threads capables de faire cette tâche en même temps, le temps de traitement sera toujours d'au moins 300 ms. Créer une instance de Tesseract prend également du temps. Ma mission n'est donc pas d'arriver à des temps de quelques dizaines de millisecondes, mais plutôt de rajouter le moins de temps possible pendant le traitement et de tenter de faire le plus de choses possible en parallèle.
Voici la liste des choses qui prennent du temps :
- Lancement du navigateur et navigation
- Création 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és qu'une seule fois au démarrage, ce qui fait que ce n'est pas catastrophique s'ils prennent du temps. Tandis que l'OCR et le filtrage est fait à chaque détection.
Pour ce qui est du démarrage, malheureusement, on ne peut pas faire grand-chose. Lancer le browser et naviguer à travers la F1TV prend du temps, surtout si la connexion du client est mauvaise. Pour certaines actions, j'ai fait un système qui essaie pendant 10 secondes de cliquer sur un bouton plutôt que d'attendre 10 secondes et cliquer pour tenter d'économiser un peu, mais malheureusement, c'est lent et on ne peut pas y faire grand-chose.
Pour la génération 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élisable), si on veut faire plusieurs reconnaissances à la fois, il faut plusieurs instances de Tesseract loadées en mémoire. J'ai donc décidé, pour une question de simplicité et de performances, de faire en sorte que chaque fenêtre de donnée ou "Window" aie sa propre instance de Tesseract.
Vous qui lisez ces lignes êtes peut-être en train de vous dire "Oulala mais ça doit beaucoup de mémoire son truc là " et vous auriez parfaitement raison !
Ce programme consomme en effet une quantité absolument catastrophique de mémoire vive. Mais si je l'ai fait, c'est pour une bonne raison. Cela prend juste beaucoup trop de temps de créer une nouvelle instance à chaque boucle de Tesseract et c'est encore plus long de faire toutes les opérations d'OCR les unes après les autres pour n'avoir qu'un seul Tesseract de loadé.
On peut parfois arriver à des chiffres qui approchent les 4GB de RAM ce qui est absolument RIDICULE. Cependant, c'est un compromis que j'étais prêt à faire pour avoir une application qui soit plus rapide.
Je suis absolument certain que cette solution et les autres solutions que j'ai trouvées pour ce projet ne sont pas les meilleures ou les plus efficaces. Mais ce sont les solutions que j'ai trouvé pour faire en sorte que le projet avance et fonctionne à peu près vite.
Ensuite pour ce qui est de ce qui se passe à chaque boucle, là le mot magique, c'est "Parallèle". Le traitement de toutes les zones est fait en même temps.
La structure du projet en zones, sous zones et fenêtres de données fait qu'il est assez facile de venir paralléliser le processus si on les implémente correctement.
On peut voir sur ce diagramme que la zone principale demande à toutes les sous zones de décoder leur contenu. Ces dernières font l'exacte même chose avec les fenêtres de données qui retournent chacune ce qu'elles contiennent après un coup d'OCR et ensuite les zones recombinent les informations et les envoient à la zone principale.
Tout cela est très bien, mais quel rapport avec la parallélisation ? Eh bien, comme chaque zone de pilote est indépendante, on peut tout simplement faire une boucle for parallèle qui appelle toutes les zones pilotes.
On passe de 15 à 20 secondes de traitement à un peu plus de trois juste avec cette technique. Alors ça n'était pas simple à implémenter, car il a fallu programmer les zones de sorte qu'elles soient toutes indépendantes les unes des autres. Mais une fois que le travail en amont a été effectué, il est très simple de paralléliser.
Les filtres fonctionnent de la même façon sauf que là, on parallélise 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érence.
Seul souci avec cette méthode, cela veut dire que le processeur est particulièrement sollicité '^^...
Mon laptop ne possède 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 à d'encore meilleurs résultats.
Mais cette utilisation du processeur a aussi un inconvénient...
Donc si je veux commenter la F1 avec cet outil, note à moi même, je ne dois pas utiliser le laptop si je ne veux pas me cramer les doigts.
Si je pouvais utiliser le GPU pour accèlérer le processus on pourrait peut-être avoir de meilleurs résultats mais de ce que j'ai pu lire, l'OCR n'est pas spécialement un bon use case pour les GPU.
Pour conclure, je dirais que ce projet est loin d'être un exemple de performances et clairement, il y a des choix discutables qui ont été faits et d'une manière générale, si je devais refaire tout le projet avec la performance en premier objectif, j'aurais sûrement fait différemment. Maintenant, avec le temps que j'ai eu, je suis déjà content d'avoir pu faire quelque chose qui fonctionne et qui ne prenne pas une minute à traiter une image.
Ethique du projet
Ici, on va parler des questions éthiques de ce projet. En effet, il y a quelques petites choses qui peuvent soulever une question.
Il y a deux questions qui reviennent presque à chaque fois que je parle ou présente mon projet :
Utilisation abusive de la F1TV ?
La F1TV est un service payant qui n'est pas forcément donné (même 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ès plus facile ou faire fuiter des informations de courses que l'on ne peut se procurer que par son utilisation.
Mais voilà 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é récemment sur sa machine. (Cela veut donc dire que je ne permets pas à des utilisateurs de frauder)
- L'application ne partage aucune information sur le contenu de la F1TV avec l'extérieur. (On ne peut pas avoir accès à des informations payantes sans abonnement)
- L'application ne simule qu'un seul utilisateur connecté sur une vraie machine (Cela veut donc dire que je ne suis pas en train de faire un système de bot qui regarde 45 flux en même temps pour scrapper tout le site et/ou poser des problèmes de DDOS)
- Les données ne sont pas stockées entre les sessions (cela veut dire que l'on ne représente pas un risque de fuite de données 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 à côté de nous la regardait en prenant des notes pour nous aider à suivre. Je ne vois donc pas le mal et je ne vois pas en quoi ce projet serait problématique sur ce point.
Après dans le futur, le but est clairement de conserver les infos trouvées pour entrainer un algorithme de prédiction et là peut-être que cela pourrait poser plus de problèmes, mais ce n'est pas le cas à l'heure ou j'écris ces lignes.
Récupération de cookies à l'insu de l'utilisateur ?
Alors là, on est clairement sur le sujet un peu plus épineux...
Un peu de contexte d'abord :
À 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èmes à cette solution :
- L'utilisateur doit avoir assez confiance en mon programme pour laisser ses identifiants en clair à l'intérieur.
- Il est extrêmement difficile de bypass la protection contre les bots de la page de login de la F1TV.
J'ai donc dû trouver une autre solution : Utiliser les cookies !
Le seul souci, c'est que cela voulait dire que l'utilisateur devait aller chercher lui-même ses cookies dans le navigateur en utilisant F12 et qu'il devait à nouveau me faire confiance pour que je n'en fasse rien. Je trouvais cette solution trop pénible pour l'utilisateur alors, j'ai décidé d'en trouver une autre.
Utiliser les cookies MAIS, sans demander à l'utilisateur. Pour faire simple, mon programme va directement décoder les cookies encryptés dans la base de donnée SQLITE de Chrome, va les stocker dans un CSV en clair et va laisser mon programme C# aller piocher ceux qui l'intéressent.
Soucis, mon programme a accès à tous les cookies de l'utilisateur à son insu, cela veut dire que je pourrais les utiliser à des fins peu scrupuleuses.
Et c'est la solution que j'ai décidé de choisir, car elle permet à l'utilisateur de ne rien avoir à 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 ça va. :D
Non plus sérieusement, 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évois jamais de le faire.
Mais il est intéressant de mentionner que mon application met en péril la sécurité des cookies de l'utilisateur et qu'il serait bien dans le futur de mettre un message explicatif au premier démarrage ou dans l'installeur de l'application pour prévenir l'utilisateur.
Utilisation de Chat GPT
Cette année, ChatGPT est venu s'installer dans la liste des outils que j'utilise presque quotidiennement pour avancer sur mes projets.
J'ai utilisé 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ès précis, ChatGPT est une ressource absolument géniale.
Je l'ai surtout utilisé 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 à ce qu'on lui donne.
Par exemple, il m'est souvent arrivé 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ès rapide, il a quasiment toujours pu me fournir le code minimum. Cependant, dès que l'on arrive sur des cas encore plus précis, on atteint assez vite les limites du système.
J'ai fréquemment fait appel à cet outil pour diagnostiquer du code, que ce soit pour détecter un souci ou même plus juste pour voir si mon code avait du sens. En effet, si on donne une méthode à chatGPT, il va tenter de l'expliquer, et s'il n'y arrive pas, c'est généralement que les variables sont mal nommées ou qu'il y a un souci avec la logique du code. Et pour ce qui est de la détection des erreurs, l'exemple que je peux donner c'est quand je faisais des méthodes asynchrones et parallèles, je pouvais lui donner la méthode 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 à la fin l'outil est assez limité et je ne l'utilisais que quand mes recherches internet étaient infructueuses.
Le seul cas où il m'a un peu sauvé, c'est quand je travaillais avec Puppeteer et que j'essayais de régler un souci qui faisait que le programme plantait à chaque fois que j'ouvrais une vidéo. Au bout de quelques heures de galère, il m'a juste proposé d'utiliser une autre librairie comme sélénium et il m'a converti tout mon code puppeteer en code utilisable par Selenium, et même si cela a demandé un peu plus de travail que de copier-coller, pour finir, j'ai pu avoir quelque chose qui marchait et je n'aurais peut-être pas eu le réflexe ou l'envie de le faire si je n'avais pas utilisé cet outil.
En conclusion, certaines méthodes de mon projet ont été faites avec l'aide de ChatGPT mais c'est une minorité et je l'ai surtout utilisé pour comprendre des erreurs et pour avoir des pistes à explorer pour les fix. Rien de bien fou.
Améliorations futures
Ici, je vais parler de deux types d'améliorations. Les améliorations à court terme, que j'aurais pu faire si je n'avais pas perdu autant de temps sur certains problèmes techniques ou si j'avais eu quelques semaines de plus pour travailler sur le projet. Et les idées qui seraient plus compliquées à mettre en place que je n'aurais jamais pu ajouter à ce travail dans le temps imparti, mais qui sont maintenant possibles si je continue pendant quelques mois à travailler sur le projet.
Court terme
Je vais commencer par les petites améliorations.
- Chose que je regrette le plus, je dirai, c'est tout ce qui est affichage. J'aurais vraiment aimé faire une magnifique interface, mais il m'a manqué de temps pour en faire une plus jolie et plus facile d'utilisation.
- Une amélioration réellement nécessaire serait d'améliorer la détection des pneus pour qu'il soit possible de correctement détecter les arrêts aux stands.
- En général, si j'avais pu mettre plus de temps dans l'analyse des données que je reçois de la F1TV, j'aurais pu faire un système plus efficace de détection de dépassements, car la version actuelle n'est vraiment pas bonne.
- Trouver un moyen de faire des erreurs plus précises. En effet, maintenant, certaines erreurs ont des causes qui peuvent être multiples (qui peuvent être causées par un mauvais lien, ou une erreur de récupération des cookies ou même juste de connexion internet). Ça demanderait simplement un peu plus de temps pour qu'au lieu de retourner seulement une erreur, on tente de récupérer plus d'infos pour la rendre plus spécifique.
Et pour les améliorations un peu plus concrètes :
- Implémenter plus d'affichages calculés. J'aurais aimé ajouter des affichages comme le classement pondéré des pilotes en fonction des arrêts aux stands. Cela demanderait juste un peu de temps et d'améliorer la détection des pitstops.
- Implémenter des affichages prédictifs simples. On pourrait imaginer des algorithmes simple qui pourraient tenter de prédire quand un pilote va en rattraper un autre ou quand un pilote va devoir s'arrêter en fonction des temps aux tours. Ça ne me demanderait pas de nouvelles technologies, mais simplement du temps pour mettre en place et tester les algorithmes.
- Faire un système qui puisse tester les algorithmes prédictifs sur un panel de Grand Prix. Si l'étape d'avant est faite, on peut facilement imaginer un bout de programme qui aille tester le programme sur différents Grand Prix pour voir si les prédictions sont bonnes.
- Avoir une notion d'historique des courses pour avoir une page de comparaison des performances des équipes. Par exemple, déterminer quelle voiture est la plus rapide et comparer avec les autres circuits. On peut même imaginer qu'après plusieurs Grands Prix, on puisse tenter de déterminer quelle équipe est forte sur quel circuit.
- Avoir un système qui permet de trouver automatiquement tous les liens de Grand Prix comme ça l'utilisateur n'aie plus besoin d'aller chercher un URL.
- Faire un installer pour qu'un utilisateur n'ait pas à se taper la procédure d'installation (qui est assez pénible) à la main.
Long terme
Là, on va se pencher sur des features qui prendraient plus d'un mois à mettre en place correctement.
- On pourrait imaginer un système 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ème qui puisse créer des infographies. Que ce soit au milieu de la course ou à la fin, le programme pourrait nous générer des images avec une stat intéressante (ex : x pilote a fait x dépassements ou x pilote gagnerait x points s'il finissait dans cette position, ce qui le ferait changer de position au classement général). Si c'est bien fait, cela pourrait être un outil extrêmement précieux, car je pourrais utiliser ces infographies dans mes commentaires.
- On pourrait avoir un système 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énial d'avoir une page de stats qui se souviennent de tous les anciens Grand Prix regardés qui permettent d'afficher toutes les stats d'un pilote sur plusieurs courses. (Cela me permettrait, dans des moments où la course stagne un peu, de pouvoir prendre n'importe quel pilote et d'avoir des choses à dire à son sujet)
- On pourrait même imaginer un système qui utilise une base de données sur un serveur Infomaniak et développer une extension de navigateur qui me donne des infos importantes directement sur la page où 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é.
Je vais m'arrêter là parce que les possibilités sont tout simplement infinies. À partir du moment où je peux récupérer toutes les informations de la F1TV de manière 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émenter toutes ces améliorations et plus.
Je pense vraiment que si je continue à commenter pour le 20 minutes dans les années qui viennent, cela pourrait être intéressant de développer un outil du style qui pourrait grandement m'aider à faire des commentaires de qualité.
Conclusion
Bilan
Je vais faire un petit bilan de ce travail. Déjà, je vous remercie chaleureusement d'avoir lu cette documentation (j'ai dû la relire en entier une ou deux fois, je sais que ce n'est pas facile). J'espère que j'ai pu parler de tout ce dont je voulais parler et que je l'ai fait de manière explicite et aisé à lire pour vous. J'ai réellement fait de mon mieux pour qu'elle soit la plus simple possible à lire, mais c'est un exercice difficile dans un document de cette taille et je m'excuse des inévitables erreurs et coquilles que vous aurez peut-être remarqué.
Je dois avouer que je suis quand même très content d'arriver au bout de ce travail. J'ai vraiment aimé cette expérience unique de pouvoir travailler à 100 % sur un projet et voir de quoi je suis capable. Mais je suis aussi heureux d'arriver à la fin, car je dois avouer que ça n'a pas été simple tous les jours et que de travailler presque seul sur un projet si long n'est pas facile.
Pour être tout à fait honnête, je suis quand même fier de ce que j'ai fait (ce qui n'arrive pas souvent). C'est un projet qui est à des années lumières de la perfection, mais c'était mon idée et en commençant le projet, je ne savais même pas si j'allais y arriver. Certes le résultat n'est pas exactement comme je l'aurais rêvé, mais il est concret et il fonctionne !
Il y a eu des moments ou en voyant la quantité de choses qu'il restait à faire, je me sentais un peu découragé, mais je suis arrivé au bout avec un projet fonctionnel et pour ça, je suis assez fier.
Ce fut un projet difficile, surtout sur le plan de la résolution de problèmes. Chaque étape du projet apportait une nouvelle problématique qu'il fallait résoudre et si parfois, j'ai pu trouver des façons élégantes de le faire, pour d'autres, il a fallu être un peu plus créatif et moins regardant sur la méthode, mais que sur le résultat.
Je suis un peu frustré de rendre le projet alors que j'ai encore pleins d'idées pour le rendre meilleur. Mais je suis content de rendre quelque chose qui fonctionne et qui est déjà techniquement utilisable sur le terrain.
Ce projet m'a également appris pas mal de chose sur ma manière de travailler et sur la gestion de projet et je sais que tous mes futurs projets bénéficieront de ces apprentissages.
Résumé des épreuves
Ici, je vais tenter de résumer très rapidement tout ce qui a dû se passer pour en arriver là.
Pour commencer, il a fallu trouver un moyen de récupérer des images de la F1TV automatiquement. Pour ce faire, j'ai dû trouver une librairie qui me permette de contrôler un navigateur Firefox. Il a ensuite fallu trouver un moyen de se connecter automatiquement, pour ce faire, j'ai dû écrire un bout de code Python qui est allé chercher les cookies dans la base de données de chrome. Ensuite, il a fallu réussir à 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ésolution.
Avec ces images, il a ensuite fallu développer un système qui permette à l'utilisateur d'indiquer au programme où se trouvaient les informations. Il a ensuite fallu faire un système qui utilise ces informations pour découper l'image pour isoler les infos et les envoyer à la partie reconnaissance.
Cette partie reconnaissance a dû être développée de manière quasi unique pour chaque type d'information reconnue et en plus de la partie reconnaissance qui était déjà bien galère, il a fallu faire tout un système qui puisse détecter les anomalies de reconnaissances pour être sûr que les informations récupérées étaient bonnes.
Après tout ça, il a fallu faire en sorte que ces données soient stockées et affichées correctement. Créer une façon de les afficher de manière utile et facile à l'utilisateur.
Et tout ce beau monde a dû être optimisé pour que l'application ne prenne pas une minute pour récupérer 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ésumé un peu barbare qui oublie énormément de choses et qui ne parle pas des problèmes rencontrés, mais cela peut donner une vague idée de la taille du projet et de pourquoi je suis déjà si fier, juste que tout fonctionne.
Merci d'avoir lu cette documentation, j'espère qu'elle a été instructive et je vous souhaite une excellente journée
Notes de code
Ici, je vais donner quelques petites infos qui pourraient vous être utiles si vous décidez d'aller vous aventurer dans mon code source.
Le programme n'est pas à proprement parlé un programme en MVC, le découpage général suit quand même cette philosophie, je vais donc les ranger de cette façon pour que ça soit plus simple pour vous de comprendre.
Vues
Comme le projet n'est pas un MVC parfait, les vues font quand même quelques actions, mais les deux fichiers dont je vais parler ici sont à au moins 90 % juste de la vue
Settings.cs
Ce fichier contient tout le code pour contrôler la vue des "Settings" qui est la vue qui se charge de la création et édition des Presets. Si vous voulez changer le comportement de cette page, il faut éditer ce fichier.
Cette vue utilise deux contrôleurs :
- F1TVEmulator
- ConfigurationTool
Le premier pour pouvoir lancer une instance de Firefox qui permet de tester le système, le second pour effectuer toutes les actions de création, modification ou de lecture des "Presets"
Rien de bien fou à dire sur ce fichier. La seule chose un peu bizarre est la gestion de la création des zones et des fenêtres. Il y a tout un système qui peut être un peu bizarre à première vue qui sert à détecter quand l'utilisateur clique sur l'image pour créer une zone. Je suis sûr qu'il existe une manière plus propre de le faire que celle que j'ai utilisée, mais j'ai fait en sorte que cela fonctionne.
Un truc qui serait bien à ajouter dans le futur serait un moyen de visualiser au moins les points que l'on ajoute au fur et à mesure plutôt que de tout voir à la fin.
Form1.cs
Ce fichier contient tout le code pour contrôler la vue principale. Elle se charge de lancer le navigateur et d'afficher toutes les données récupérées ou stockées.
Cette vue utilise deux contrôleurs :
- F1TVEmulator
- DataWrapper
Le premier pour contrôler le navigateur (le lancer, le stopper, changer l'URL etc.) et le second pour accéder à des infos de la base de donnée sans avoir à l'appeler directement.
Contrôleurs
Ces classes ne sont pas des contrôleurs à 100 %, car ils contiennent aussi un peu de calcul, etc. mais ont comme but principal de servir d'interface entre la vue et les données.
ConfigurationTool.cs
Cette classe sert à travailler avec la zone principale pour la contrôler et à contenir les méthode qui servent à la création de Presets.
Les deux grosses méthodes que cette classe contient sont :
- SaveToJson
- AutoCalibrate
La première sert tout simplement à 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ù il y a du texte et fait une calibration auto pour créer automatiquement les zones de pilotes.
Les autres méthodes sont juste des méthodes qui appellent des méthodes de modèles et servent seulement d'interface.
DataWrapper.cs
Cette méthode sert à faire l'intermédiaire entre la form principale et le contrôler "Reader" ainsi que la classe qui contrôle directement la base de données.
Elle interface avec ces deux classes :
- Reader
- Storage
Reader est un genre d'hybride, mais qui se veut être un genre de contrôler de la lecture des données sur les images et des fichiers JSON tandis que storage est le modèle qui interagis directement avec la base de données SQLITE.
Cette classe contient des méthodes qui auraient très pu (et sûrement dûes) se retrouver directement dans la vue. La plupart des méthodes sont là pour générer des contrôles qui contiennent des informations récupérées par la base de données ou par l'OCR.
Reader.cs
Cette méthode 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êtres de données.
C'est cette méthode qui va gérer la classe Zone, qui va demander à la classe zone de modifier, ajouter ou supprimer des fenêtres etc.
Elle contient aussi des méthodes pour charger un "Preset" et dessiner sur les Images quand une vue en a besoin.
Zone.cs
Cette méthode est clairement la plus discutable en tant que contrôleur, mais qui est en même temps la plus proche.
La raison est qu'elle peut être 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ée par un contrôleur et qui retourne des infos. Mais quand elle est utilisée comme une zone principale, c'est l'orchestre de toutes les zones et fenêtres.
Dans ce dernier cas, c'est un intermédiaire entre les zones et fenêtres. Elle ne sert qu'à contrôler des sous zones et leurs fenêtres.
Les seules méthodes de cette classe servent à demander des informations aux sous zones/fenêtres. Il n'y a quasi aucun calculs.
Modèles
Là, 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épendantes et contiennent toutes des méthodes et des infos très différentes, tandis que les classes dérivées de Window.cs ont toutes la même structure et ont comme seul et unique but de retourner ce qui est marqué dans leur image.
Il est donc normal que ces dernières se ressemblent beaucoup.
DriverDrsWindow.cs
Cette classe est prévue pour contenir une image dans laquelle on peut voir l'état du DRS d'un pilote.
La méthode qu'elle utilise pour savoir si le pilote a activé son DRS ou non est d'utiliser la moyenne de couleur de son image.
Elle retourne true ou false et elle contient elle-même toutes les méthodes qui sont nécessaires pour donner une réponse (c'est un cas rare).
DriverGapToLeaderWindow.cs
Cette classe est prévue pour contenir une image dans laquelle on peut voir combien de temps sépare le pilote actuel du pilote devant lui.
La méthode qu'elle utilise pour le savoir utilise de l'OCR et fait appel à une méthode contenue dans son parent Window.
Elle est plutôt vide, car tout le traitement est déporté dans son parent.
DriverLapTimeWindow.cs
Cette classe est prévue pour contenir une image dans laquelle on peut voir quel était le dernier temps au tour enregistré du pilote.
La méthode qu'elle utilise pour le savoir utilise de l'OCR et fait appel à une méthode contenue dans son parent.
Elle est plutôt vide, car tout le traitement est déporté vers son parent.
DriverNameWindow.cs
Cette classe est prévue pour contenir une image dans laquelle on peut voir le nom du pilote écrit en toutes lettres.
La méthode qu'elle utilise une partie d'OCR qui est déportée dans le parent et utilise aussi une méthode appelée IsADriver (qui aurait pu aussi être déportée dans la page principale) qui vérifie si le nom trouvé existe.
DriverPositionWindow.cs
Cette classe est prévue pour contenir une image dans laquelle on peut voir la position d'un pilote.
Cette méthode est également un peu vide, car pour décoder l'image le traitement est déporté dans son parent.
DriverSectorWindow.cs
Pareil que pour DriverPositionWindow.cs
DriverTyresWindow.cs
Cette classe est prévue pour contenir une image dans laquelle on peut voir l'infographique qui représente le pneu du pilote.
Cette méthode est la seule fenêtre intéressante, car elle utilise du code déporté dans le parent, mais aussi une certaine proportion qu'elle contient elle-même.
Elle contient des méthodes qui permettent par exemple de trouver la zone intéressante dans l'image ou choisir quel pneu un pilote chausse en fonction de la couleur moyenne de l'image de la zone trouvée.
Pour toutes les zones de type Window, ce qui est vraiment intéressant, 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ête, de cliquer sur un bouton après 34 secondes que de récupérer les cookies qui permettront de se connecter ensuite.
Voici les méthodes qui s'occupent des cookies :
- StartCookieRecovering
- GetCookie
Je déconseille de modifier ces deux méthodes. Elles ont une utilité très claire et elles fonctionnent. (If its not broken dont fix it)
Ce qui peut être intéressant en revanche, c'est la seule autre méthode que cette classe propose sobrement intitulée "Start".
Cette méthode est codée de manière totalement procédurale et décrit exactement toutes les actions à faire à partir du moment ou le navigateur est démarré, dans quel ordre et s'il faut les faire ou non. Si vous vouliez modifier quelque chose ici, je pense que la bonne idée serait une meilleure gestion des erreurs. Pour le moment, si le programme n'arrive pas à cliquer sur certains boutons, soit une erreur est lancée, soit on attend un peu avant de réessayer. La vraie chose qui manque, c'est la raison pour laquelle ces boutons n'ont pas pu être cliqués. Dans l'idéal, il faudrait ajouter un système qui peut détecter la panne exacte pour que le message d'erreur soit plus personnalisé.
Sinon c'est une méthode qui marche plutôt bien et qui est faite complètement sur mesure pour l'utilisation de la F1TV.
OcrImage.cs
Là, on attaque les classes un peu plus "bordéliques".
Cette classe regroupe toutes les actions de filtrage que l'on pourrait vouloir. Cette classe est pas mal utilisée pour l'OCR. Il n'y a que deux choses à savoir.
- Presque toutes les méthodes de filtres sont génériques et peuvent être utilisées à peu près n'importe où 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éthode qui va vous intéresser si vous voulez changer le comportement de l'OCR est la méthode "Enhance".
La méthode enhance est un genre de mode d'emploi. Selon le contexte de l'image (si c'est une image qui vient d'une fenêtre de DRS, de temps au tour, de pneu etc.) il y aura une combinaison de filtres différente.
Plusieurs méthodes dans cette classe ne sont pas utilisées, mais sont gardées, car elles pourraient être utiles. La plupart du temps, l'utilisation de ces filtres est décidée avec des essais à tâtons. Vous comprendrez donc vite que c'est mieux de garder sous le code des méthodes car certaines combinaisons marchent mieux que d'autres.
SqliteStorage.cs
Cette classe est plutôt simple.
Ce sont simplement toutes les méthodes qui permettent de créer, éditer et accéder à la base de données SQLITE.
Vous y trouverez des méthodes qui sont juste là pour créer la base comme d'autres plus spécifiques qui sont un peu plus spécifiques comme celles qui veulent récupérer l'ID d'un pilote selon son nom ou celle qui veut récupérer l'historique des temps autour d'un pilote.
Rien de spécial à 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êtres pourraient avoir besoin.
On retrouve des méthodes pour calculer la différence entre deux strings qui peut servir pour aider à la reconnaissance de noms de pilotes ou bien une méthode qui permet de convertir une image en tableau de bytes.
La méthode la plus grosse cependant et de loin est la méthode GetTimeFromPng qui doit implémenter un système qui permet de détecter quand un temps est anormal et détecter si la raison est la mauvaise compréhension d'une ponctuation ou le rajout d'un chiffre. Cela prend énormément de place, car il y a beaucoup de cas particuliers et il a fallu tout coder à la main. Je déconseille à qui que ce soit de lire cette méthode, ainsi, elle pourrait causer de sévères dommages au cerveau humain.
À écrire, ce fut une horreur, à comprendre, je n'ose pas imaginer.
Sinon pas grand-chose de plus à raconter.
Structures
Les classes de structures sont des classes qui ne contiennent que peu ou pas de traitement et qui sont simplement là pour contenir des informations. Elles sont pratiques, car elles permettent de rendre le code dans les autres classes beaucoup plus lisible et leur éviter d'utiliser des tuples bizarres.
DriverData.cs
Cette classe contient toutes les infos d'un pilote à un moment donné.
On peut voir cette classe comme une classe contenant une ligne de la F1TV. Toutes les données à propos d'un pilote que l'on peut détecter en une détection sont stockées là-dedans.
Il n'y a pas de notion d'historique ou quoi que ce soit. C'est simplement un moyen de stocker des données de pilotes dans d'autres classes en ayant un nom logique et aider à la lecture. Pas réellement de traitement.
Ce fichier contient également un autre objet : Tyre.
Cet objet contient les infos d'un pneu, rien de plus.
Et voilà, ce fut un résumé extrêmement 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ès difficile à comprendre. Bonne chance !
Glossaire
Vocabulaire F1 :
- DRS : Drag Reduction System. : Système qui permet d'ouvrir l'aileron arrière de la monoplace quand elle se trouve à une seconde ou moins de la voiture devant elle. Cela permet de réduire la trainée que la voiture subit et lui permet d'avoir un petit boost qui aide à dépasser.
- Pitstop : Arrêt aux stands : Pendant une course de F1, les pneus s'usent extrêmement vite et tous les pilotes sont obligés de passer au moins une fois par les stands par course pour les changer. Et pour changer ces pneus, ils font un arrêt 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égiques et il est très important de savoir lequel chaque pilote utilise. Les pneus Inter et Wet sont des pneus pluies, l'Inter étant pour les faibles pluies.
- Secteur : Section de circuit : Les circuits de F1 sont toujours découpés en trois parties qui sont mesurées séparément et qui permettent une meilleure granularité dans l'estimation des résultats. On n'est pas obligé 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 à une seule place, terme utilisé souvent pour décrire les F1 dans le document.
- Grand Prix : Course officielle de Formule 1. Événement 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éveloppement avec pour objectif les test. Les tests sont écrits en amont et le but du développeur est simplement de les faire passer.
- MVC : Modèle Vue Controlleur : Architecture de projet qui sépare le traitement de l'information, son affichage et sa gestion.
- Preset : (dans ce projet) Set d'informations préparées à l'avance pour être utilisés ultérieurement.
- DB : Data Base / Base de donnée
- Cookie : Fichier créé par un site internet stocké sur la machine du client qui est utilisé en général pour conserver des informations de connexion même après la fermeture du navigateur.
- Window : Fenêtre (dans ce projet) objet contenant une partie d'une image contenant une information précise.
- Zone : (dans ce projet) objet contenant une partie d'une image qui peu être sous divisée en fenêtres de données.
- 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ès précis
- AWS : Amazon Web Service : Service d'hébergement d'Amazon
- User Agent : Signature numérique du navigateur qui permet à un site de détecter le type d'appareil et de navigateur connecté (peut être changé manuellement)
- CSV : Comma Separated Values : Format de fichier qui permet de stocker facilement des données sous forme de tableau
- API : Application Programming Interface : Interface générique qui permet d'accéder à une ressource.



























































































