Updated doc

This commit is contained in:
2023-05-11 15:55:47 +02:00
parent bc11a3f051
commit b4b4756143
26 changed files with 8967 additions and 5303 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+10 -5
View File
@@ -28,17 +28,22 @@ Le but du projet est donc de fournir un outil qui hiérarchise et affiche diffé
### Abstract ### Abstract
Track Trends is a Formula 1 data and analysis tool. Track Trends is a Formula 1 data is a tool that displays and interpret data.
To understand everything, a little bit of context. In my free time I have multiple activities and one is to be the Live Ticker F1 for the local journal "20 minutes". to help me in this work I'm currently using the F1TV to which I'm currently subscribed because it provides me with a better video feed with better commentary than the ones from the RTS (in my opinion) but also because it gives me access to a very important video feed : the data channel To understand everything,first ,a little bit of context. In my free time I have multiple activities and one is to be the Live Ticker F1 for the local journal "20 minutes" (Owned by Tamedia). to help me in this work I'm currently using the F1TV to which I'm currently subscribed because it provides me with a better video feed with better commentary than the ones from the RTS (in my opinion) but also because it gives me access to a very important video feed : the data channel
See the screenshot above to see what it looks like. You can see in the chapter above an example of the F1TV DATA CHANNEL.
[note: It's a pretty HTML table but a full on video feed that contains a table (probably, so you can't access data directly)] [Note : Even tough it looks like a pretty HTML table on wich you could easely get infos... Its not. Its a video feed]
You can see a lot of data all well and good BUT! All the data is displayed the same in a big table which make it really hard to read totally in a hurry, which means that I miss a lot of useful information. You can see a lot of data all well and good BUT! All the data is displayed the same in a big table which make it really hard to read totally in a hurry, which means that I miss a lot of useful information.
The point of the project then is to provide with a tool that can display those data by taking into account their relevance. That would help me not miss any and provide a better commentary by never missing out battles, and be able to better write with the time I saved by using it. The point of the project then is to provide a tool that can display those data by taking into account their relevance.
So for example a driver that is 10s away from everyone and that is doing some normal lapTimes will be less displayed or even not displayed at all so I can focus on the drivers that are battling each others.
This tool would help me not miss the battles and details that are happening in the back and therefore not being broadcasted on TV.And it could be a usefull tool for anyone who wants a better insight of how the race is going by looking at the data.
This kind of project already exists in the form of the AWS tool "F1 Insight" but it is not avaible to the public. We can only see some of its predictions (that are trash) and data dumps in the live feed when the TV directors feel like it.
### Description du besoin ### Description du besoin
+430
View File
@@ -2188,3 +2188,433 @@ Bon au final j'ai quand même changé mon poster
Mais je suis trop attaché à l'ancien concept alors je vais plutôt utiliser ca : Mais je suis trop attaché à l'ancien concept alors je vais plutôt utiliser ca :
!["Poster V9"](./Images/Figma/PosterV9.png) !["Poster V9"](./Images/Figma/PosterV9.png)
Je pense que cette version est meilleure même si elle est encore plus en bordel par ce que le texte permet de se faire une meilleure idée de l'utilisé de chaque partie.
## Mercredi 10 Mai 2023
Bon hier je n'ai pas eu le temps de finir la documentation de la recupèration d'images et de la calibration. Il faudra donc que je repasse un coup dessus en fin de semaine je pense.
Mais la j'aimerais avancer sur la mise en commun du projet, comme la configuration fonctionne plutôt pas mal je pense que je vais juste vite fait aller commenter les methodes qui ne le sont pas encore et ensuite je vais passer à l'implémentation de l'OCR.
Je suis presque certain que l'OCR va avoir besoin de plus de règlages mais bon on verra bien.
Je me rend compte en commentant que la methode de load serait plus efficace avec un tout petit peu plus d'infos de la part du JSON. J'aurais pu ajouter l'offset entre chaque Driver Zone pour eviter un lèger drift lors de la reconstruction. Mais bon rien de grave donc je pense que je vais le laisser comme ca pour le moment à moins que ca me pose soucis plus tard.
J'ai eu quelques soucis avec les images en 4K. Du coup j'ai descendu les variables d'environnement à 1920x1080
En fait il y a parfois un soucis un peu pénible avec l'OCR.
Parfois pour un temps comme ci dessous:
!["1:45.140"](./Images/Screens/BadOcr.png)
Le programme ne va pas bien comprendre les ponctuations et il va donner : `1115140`
La il y a deux problèmes... Le 1:xx.xxx est compris comme 11xxxxx et le 4 s'est transformé en 1...
J'ai créé ce "petit" bout de code pour gèrer les fois ou les '.' et les ':' ont mal été interprêtés
```Csharp
if (rawNumbers.Count == 1)
{
//If this code is used it means that its bad ...
//The methods that comes are really not that great and are juste quick fixes
try
{
result = Convert.ToInt32(rawNumbers[0]);
switch (windowType)
{
case OcrImage.WindowType.Sector:
//The usual sector is in this form : 33.456
if (rawNumbers[0].Length == 6)
{
//The '.' has been understood like a number
result = 0;
result += Convert.ToInt32(rawNumbers[0][0] + rawNumbers[0][1]) * 1000;
result += Convert.ToInt32(rawNumbers[0][3] + rawNumbers[0][4] + rawNumbers[0][5]);
}
if (rawNumbers[0].Length == 5)
{
//The '.' has been overlooked
result = 0;
result += Convert.ToInt32(rawNumbers[0][0] + rawNumbers[0][1]) * 1000;
result += Convert.ToInt32(rawNumbers[0][2] + rawNumbers[0][3] + rawNumbers[0][4]);
}
break;
case OcrImage.WindowType.LapTime:
//The usual Lap time is in this form : 1:45:345
if (rawNumbers[0].Length == 6)
{
//The '.' and ':' have been overlooked
//I Know Im skipping the cases where there are more than 9 minuts but it happens so rarely that... we dont care
result = 0;
result += Convert.ToInt32(rawNumbers[0][0]) * 60000;
result += Convert.ToInt32(rawNumbers[0][1] + rawNumbers[0][2]) * 1000;
result += Convert.ToInt32(rawNumbers[0][3] + rawNumbers[0][4] + rawNumbers[0][5]);
}
if (rawNumbers[0].Length == 7)
{
//There is two possibilities
//Either 1:45.140 has been interpreted as 1145.10 or 1:451140. We will assume its the first one
result = 0;
result += Convert.ToInt32(rawNumbers[0][0]) * 60000;
result += Convert.ToInt32(rawNumbers[0][2] + rawNumbers[0][3]) * 1000;
result += Convert.ToInt32(rawNumbers[0][4] + rawNumbers[0][5] + rawNumbers[0][6]);
}
break;
case OcrImage.WindowType.Gap:
//The usual Gap is in this form : + 34.567
if (rawNumbers[0].Length == 5)
{
//The '.' has been overlooked
result += Convert.ToInt32(rawNumbers[0][0] + rawNumbers[0][1]) * 1000;
result += Convert.ToInt32(rawNumbers[0][2] + rawNumbers[0][3] + rawNumbers[0][4]);
}
break;
}
if (rawNumbers[0].Length > 6)
{
//The number definitely has been interpreted wrong
}
}
catch
{
//It can be because the input is empty or because its the LEADER bracket
result = 0;
}
}
else
{
//Auuuugh
result = 0;
}
```
```Csharp
ConfigFile = "./Presets/Clean_2023.json";
string gpUrl = "https://f1tv.formula1.com/detail/1000006688/2023-azerbaijan-grand-prix?action=play";
```
Bon je n'arrive pas à faire fonctionner l'OCR sans tout faire crash à chaque fois. Je vais abandonner le travail de la journée pour revenir au point initial... C'est très frustrant mais bon je ne vois pas comment faire mieux. Rien ne marche alors qu'avant ca marchant super sur le projet OCR normal.
Va savoir pourquoi même comme ca, impossible de faire marcher l'OCR. Il y a un soucis au niveau de l'ASYNC qui me fait crash tout le temps en me disant qu'un objet est deja en train d'être utilisé. Ca marchait nikel dans mes premières version je ne vois pas pourquoi ca pête maintenant.
Je pense que je vois à peu près le soucis.
```Csharp
public virtual async Task<DriverData> Decode(List<string> driverList)
{
int sectorCount = 0;
DriverData result = new DriverData();
Parallel.ForEach(Windows, async w =>
{
// A switch would be prettier but I dont think its supported in this C# version
if (w is DriverNameWindow)
result.Name = (string)await (w as DriverNameWindow).DecodePng(driverList);
if (w is DriverDrsWindow)
result.DRS = (bool)await (w as DriverDrsWindow).DecodePng();
if (w is DriverGapToLeaderWindow)
result.GapToLeader = (int)await (w as DriverGapToLeaderWindow).DecodePng();
if (w is DriverLapTimeWindow)
result.LapTime = (int)await (w as DriverLapTimeWindow).DecodePng();
if (w is DriverPositionWindow)
result.Position = (int)await (w as DriverPositionWindow).DecodePng();
if (w is DriverSectorWindow)
{
sectorCount++;
if (sectorCount == 1)
result.Sector1 = (int)await (w as DriverSectorWindow).DecodePng();
if (sectorCount == 2)
result.Sector2 = (int)await (w as DriverSectorWindow).DecodePng();
if (sectorCount == 3)
result.Sector3 = (int)await (w as DriverSectorWindow).DecodePng();
}
if (w is DriverTyresWindow)
result.CurrentTyre = (Tyre)await (w as DriverTyresWindow).DecodePng();
});
return result;
}
```
Ca c'est ma methode de decoding de chaque Driver Zone. Le message d'erreur me parle d'une windowImage quand il dit qu'un objet est déja utilisé. Ma conjecture c'est que en essayant de faire toutes les windows en même temps. Elles veulent parfois accèder à l'image principale en même temps. Ce qui evidemment pose problème. Je pense que le fix le plus simple serait de faire le traitement sans le parallele quitte à exporter ce fonctionnement sur chaque zone en elle même pour ne pas perdre trop de performances.
Ok je crois que je vois ou est le soucis. En fait dans cette version du programme c'est toujours la première image qui était juste tout le temps prise et dans la première image on a une partie des chiffres qui est bloquée par l'UI de la fenêtre... lol...
EN FAIT
J'avais un soucis dans ma gestion des chiffres mal faits. Visiblement parfois quand je ne prenais pas en compte un :, un LapTime etait compris comme un Gap to leader ou un Secteur
Bon j'en ai tellement marre... Je n'arrive tout simplement PAS à faire fonctionner l'OCR ca crash tout le temps j'en peux plus.
J'ai tenté de règler les problèmes de mauvaises detections de secteurs et temps au tour qui font crasher l'app :
```Csharp
if (rawNumbers.Count == 2)
{
//ss:ms
result = (Convert.ToInt32(rawNumbers[0]) * 1000) + Convert.ToInt32(rawNumbers[1]);
if (result > (60000 + 999))
{
if (windowType == OcrImage.WindowType.LapTime)
{
result = 0;
result += Convert.ToInt32(rawNumbers[0][0]) * 60000;
result += Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString()) * 1000;
result += Convert.ToInt32(rawNumbers[1]);
}
if (windowType == OcrImage.WindowType.Sector)
{
int seconds = 0;
if (rawNumbers[0].Length == 3)
{
//We have one char that we need to delete
//For no apparent reason im going to delete the first
seconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
}
else
{
seconds = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString());
}
int ms = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
result = seconds * 1000 + ms;
}
}
}
```
Mais toujours impossible de faire fonctionner cette M**** C'est juste infernal. Je pense que je vais encore tout retirer et remplacer par ce que j'ai dans mon projet OCR original. Donc c'est une journée de perdue complêtement... C'est extrêmement frustrant.
Après des heures de debug j'ai enfin réussi à faire fonctionner le programme de temps en temps. Mais j'ai toujours le soucis que l'image ne veut pas changer alors que je fais tout pour et que l'OCR est nulle à chier du coup...
## Jeudi 11 Mai 2023
Bon après une bonne nuit de sommeil je vais reprendre les choses depuis le début.
J'ai deux soucis :
- L'OCR pue du derche
- L'Image que l'on décode ne change pas
Pour la première partie j'ai ma petite théorie. Je pense que comme je donne des images 4K alors que le feed est en 1080P, il y a déja un genre d'interpolation qui est faite. Je pense donc qu'il faut que j'adapte mon engine pour qu'il fonctionne avec cette résolution.
Je me suis demandé si ca n'était pas mieux de prendre en compte les deux résolutions pour les pc un peu moins balèzes et j'ai décidé de n'en avoir rien a faire. On verra dans le futur si c'est une feature que je voudrais ajouter mais c'est en dehors du scope du diplôme je pense.
Pour la seconde partie, je pense qu'il faut que j'aille voir du côte de OCR_Decode et de OCR Tester pour voir comment je faisais. Je dois forcément oublier un truc.
Bon ca commence mal, quand je vais voir dans le projet OCR_Decode, le changement d'image est exactement le même et il fonctionne alors que de mon côté ca n'est pas le cas.
Alors deux choses. Je me rend compte que le changement d'images n'a AUCUN effet sur la detection de texte, et seconde chose, le décalage est trop grand entre les windows. Des que le soucis d'image est règlé il va falloir que je change drastiquement ma facon de stocker la config en JSON. Il faut que je conserve les écarts.
Sinon regardez ce que ca donne quand on arrive au dernier pilote :
!["Zone de pilote décalée"](./Images/Screens/BadDriverZone.png)
Je commence à devenir FOU. Je n'arrive pas à changer cette foutue image wtf... J'ai beau tenter par tous les moyens de la changer par une image noire, l'image semble toujours rester celle du départ.
Bon j'ai enfin trouvé pourquoi et je n'ai pas envie de dire comment j'ai trouvé... Je pense que l'on a tous droit à son petit jardin secret.
Maintenant ca veut dire que je peux me focus sur le concept important qui est le changement de la création et de la lecture des JSON.
Voici un exemple de preset JSON :
```JSON
{
"Main": {
"x": 40,
"y": 355,
"width": 3784,
"height": 1438,
"Zones": [
{
"DriverZone": {
"x": 0,
"y": -10,
"width": 3784,
"height": 71,
"Windows": [
{
"Position": {
"x": 47,
"y": 11,
"width": 72
},
"GapToLeader": {
"x": 445,
"y": 13,
"width": 201
},
"LapTime": {
"x": 859,
"y": 14,
"width": 221
},
"DRS": {
"x": 1094,
"y": 13,
"width": 173
},
"Tyres": {
"x": 1270,
"y": 11,
"width": 1452
},
"Name": {
"x": 2727,
"y": 11,
"width": 351
},
"Sector1": {
"x": 3083,
"y": 10,
"width": 253
},
"Sector2": {
"x": 3339,
"y": 14,
"width": 195
},
"Sector3": {
"x": 3518,
"y": 14,
"width": 250
}
}
]
}
}
]
},
"Drivers": [
"Perez",
"Leclerc",
"Sainz",
"Alonso",
"Stroll",
"Russel",
"Verstappen",
"Zhou",
"Ocon",
"Hulkenberg",
"Hamilton",
"Norris",
"Tsunoda",
"Magnussen",
"Piastri",
"Albon",
"Gasly",
"Sargeant",
"Bottas",
"De Vries"
]
}
```
Je pense que ce qui serait bien ce serait de rajouter un "offsets" qui contienne les 19 écarts restants.
Bon... la structure de ma fabrication de JSON etait trop confuse je trouve alors je l'ai complêtement refaite.
J'ai aussi abandonné l'idée de faire un fichier le plus petit possible car au final on s'en fiche et le plus important c'est que toutes les windows et les zones soient aux bons endroits.
Ca nous fait un fichier d'environs 1300 lignes mais au moins le code pour la serialisation est plutôt clean :
```Csharp
public void SaveToJson(List<string> drivers, string configName)
{
string JSON = "";
JsonObject jsonFileObject = new JsonObject();
//Creating the mainZone object
JsonObject mainZoneObject = new JsonObject();
mainZoneObject.Add("x",MainZone.Bounds.X);
mainZoneObject.Add("y",MainZone.Bounds.Y);
mainZoneObject.Add("width",MainZone.Bounds.Width);
mainZoneObject.Add("height",MainZone.Bounds.Height);
JsonArray driverZonesArray = new JsonArray();
int DriverID = 0;
foreach (Zone driverZone in MainZone.Zones)
{
DriverID++;
JsonObject driverZoneObject = new JsonObject();
driverZoneObject.Add("name","Driver"+DriverID);
driverZoneObject.Add("x", driverZone.Bounds.X);
driverZoneObject.Add("y", driverZone.Bounds.Y);
driverZoneObject.Add("width", driverZone.Bounds.Width);
driverZoneObject.Add("height", driverZone.Bounds.Height);
JsonArray windowsArray = new JsonArray();
JsonObject windowObject = new JsonObject();
foreach (Window window in driverZone.Windows)
{
windowObject.Add(window.Name, new JsonObject {
{ "x", window.Bounds.X },
{ "y", window.Bounds.Y },
{ "width", window.Bounds.Width },
{ "height", window.Bounds.Height }
});
}
windowsArray.Add(windowObject);
driverZoneObject.Add("Windows",windowsArray);
driverZonesArray.Add(driverZoneObject);
}
mainZoneObject.Add("DriverZones",driverZonesArray);
JsonArray driversArray = new JsonArray();
foreach (string driver in drivers)
{
driversArray.Add(driver);
}
mainZoneObject.Add("Drivers",driversArray);
jsonFileObject.Add("Main",mainZoneObject);
JSON = jsonFileObject.ToString();
//Saving the file
string path = CONFIGS_FOLDER_NAME + configName;
if (File.Exists(path + ".json"))
{
//We need to create a new name
int count = 2;
while (File.Exists(path + "_" + count + ".json"))
{
count++;
}
path += "_" + count + ".json";
}
else
{
path += ".json";
}
File.WriteAllText(path, JSON);
}
```
Et normalement la lecture devrait être encore plus simple.
En fait c'était pas beaucoup plus simple mais au moins maintenant ca marche. Je vais pas mettre le code de lecture ici car c'est un peu trop long donc il va falloir me croire sur parole. (Ou aller sur Git)
Bon bah on est au même endroit qu'hier...
Bon pour demain le plan de bataille ca va être :
Changer complêtement la methode "GetTimeFromPng" pour qu'elle prenne en compte toutes les possibilités de bugs et d'oubli de '.' ou de ':' mais pas selon le nombre de blocs mais selon le type de temps que l'on cherche
Pour le moment je regarde le nombre de blocs et si il y en a deux alors c'est que c'est un temps de secteur. En fait non cela peut aussi être un temps au tour qui a raté un point.
Il faut que je bosse juste un peu vite fait la dessus et que j'arrête de putain de crasher dès que un truc est pas au bon format. Ensuite quand ca aura arrêté de crasher je vais reprendre l'OCR et voir pourquoi les resultats sont nuls a chier comme ca.
Et le but c'est que demain soir j'ai une reconnaissance de caractères plus proche de ce que j'avais dans d'autres projets... J'y croit 0 mais bon l'espoir fait vivre comme on dit.
+137 -108
View File
@@ -435,6 +435,16 @@
</a> </a>
</li> </li>
<li class="md-nav__item"> <li class="md-nav__item">
<a class="md-nav__link" href="#optimisation-du-programme">
Optimisation du programme
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#ethique-du-projet">
Ethique du projet
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#ameliorations-futures"> <a class="md-nav__link" href="#ameliorations-futures">
Améliorations futures Améliorations futures
</a> </a>
@@ -894,6 +904,16 @@
</a> </a>
</li> </li>
<li class="md-nav__item"> <li class="md-nav__item">
<a class="md-nav__link" href="#optimisation-du-programme">
Optimisation du programme
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#ethique-du-projet">
Ethique du projet
</a>
</li>
<li class="md-nav__item">
<a class="md-nav__link" href="#ameliorations-futures"> <a class="md-nav__link" href="#ameliorations-futures">
Améliorations futures Améliorations futures
</a> </a>
@@ -931,12 +951,15 @@
<p>Sauf que toutes les informations sont étalées pêle-mêle sans hiérarchie ce qui fait que cela me prendrait trop de temps de tout déchiffrer à chaque fois, ce qui me fait rater des choses intéressantes.</p> <p>Sauf que toutes les informations sont étalées pêle-mêle sans hiérarchie ce qui fait que cela me prendrait trop de temps de tout déchiffrer à chaque fois, ce qui me fait rater des choses intéressantes.</p>
<p>Le but du projet est donc de fournir un outil qui hiérarchise et affiche différemment les données pour faciliter leur lecture et me permettre de faire de meilleurs commentaires.</p> <p>Le but du projet est donc de fournir un outil qui hiérarchise et affiche différemment les données pour faciliter leur lecture et me permettre de faire de meilleurs commentaires.</p>
<h3 id="abstract">Abstract</h3> <h3 id="abstract">Abstract</h3>
<p>Track Trends is a Formula 1 data and analysis tool.</p> <p>Track Trends is a Formula 1 data is a tool that displays and interpret data.</p>
<p>To understand everything, a little bit of context. In my free time I have multiple activities and one is to be the Live Ticker F1 for the local journal "20 minutes". to help me in this work I'm currently using the F1TV to which I'm currently subscribed because it provides me with a better video feed with better commentary than the ones from the RTS (in my opinion) but also because it gives me access to a very important video feed : the data channel</p> <p>To understand everything,first ,a little bit of context. In my free time I have multiple activities and one is to be the Live Ticker F1 for the local journal "20 minutes" (Owned by Tamedia). to help me in this work I'm currently using the F1TV to which I'm currently subscribed because it provides me with a better video feed with better commentary than the ones from the RTS (in my opinion) but also because it gives me access to a very important video feed : the data channel</p>
<p>See the screenshot above to see what it looks like.</p> <p>You can see in the chapter above an example of the F1TV DATA CHANNEL.</p>
<p>[note: It's a pretty HTML table but a full on video feed that contains a table (probably, so you can't access data directly)]</p> <p>[Note : Even tough it looks like a pretty HTML table on wich you could easely get infos... Its not. Its a video feed]</p>
<p>You can see a lot of data all well and good BUT! All the data is displayed the same in a big table which make it really hard to read totally in a hurry, which means that I miss a lot of useful information.</p> <p>You can see a lot of data all well and good BUT! All the data is displayed the same in a big table which make it really hard to read totally in a hurry, which means that I miss a lot of useful information.</p>
<p>The point of the project then is to provide with a tool that can display those data by taking into account their relevance. That would help me not miss any and provide a better commentary by never missing out battles, and be able to better write with the time I saved by using it.</p> <p>The point of the project then is to provide a tool that can display those data by taking into account their relevance.
So for example a driver that is 10s away from everyone and that is doing some normal lapTimes will be less displayed or even not displayed at all so I can focus on the drivers that are battling each others.</p>
<p>This tool would help me not miss the battles and details that are happening in the back and therefore not being broadcasted on TV.And it could be a usefull tool for anyone who wants a better insight of how the race is going by looking at the data.</p>
<p>This kind of project already exists in the form of the AWS tool "F1 Insight" but it is not avaible to the public. We can only see some of its predictions (that are trash) and data dumps in the live feed when the TV directors feel like it.</p>
<h3 id="description-du-besoin">Description du besoin</h3> <h3 id="description-du-besoin">Description du besoin</h3>
<p>Comme expliqué dans le résumé, je suis Live Ticker F1. Mais pour mieux comprendre le besoin que j'ai, je pense qu'il est pertinent de comprendre comment je travaille.</p> <p>Comme expliqué dans le résumé, je suis Live Ticker F1. Mais pour mieux comprendre le besoin que j'ai, je pense qu'il est pertinent de comprendre comment je travaille.</p>
<p>Pendant un Grand Prix de Formule 1 j'ai plusieurs tâches à effectuer :</p> <p>Pendant un Grand Prix de Formule 1 j'ai plusieurs tâches à effectuer :</p>
@@ -986,8 +1009,8 @@ La raison la plus probable étant qu'Amazon avec son service AWS propose exactem
<p>Mais comme je possède un abonnement premium ++ à la F1TV, j'ai accès pour chaque grand prix à un flux vidéo nommé : DATA F1 CHANNEL</p> <p>Mais comme je possède un abonnement premium ++ à la F1TV, j'ai accès pour chaque grand prix à un flux vidéo nommé : DATA F1 CHANNEL</p>
<p>Qui ressemble à ça :</p> <p>Qui ressemble à ça :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/Formula1DataChannel.png"><img alt="" channel="" data="" exemple""="" src="./Images/Screens/Formula1DataChannel.png"/></a> <a class="glightbox" href="./Images/Screens/Formula1DataChannel.png"><img alt="" channel""="" data="" de="" exemple="" la="" src="./Images/Screens/Formula1DataChannel.png"/></a>
<figcaption>"Data channel exemple"</figcaption> <figcaption>"Exemple de la Data Channel"</figcaption>
</figure> </figure>
<p>Donc la seule façon que je vois de récupérer ces données est de les prendre directement sur ce feed.</p> <p>Donc la seule façon que je vois de récupérer ces données est de les prendre directement sur ce feed.</p>
<p>Même si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout être la récupération des données et leur stockage.</p> <p>Même si le but final de l'application est de faire pleins de choses super avec les datas, le gros du projet va surtout être la récupération des données et leur stockage.</p>
@@ -1017,8 +1040,8 @@ La raison la plus probable étant qu'Amazon avec son service AWS propose exactem
</ul> </ul>
<p>Voici un exemple d'interface possible pour une page :</p> <p>Voici un exemple d'interface possible pour une page :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Figma/Prototype.png"><img alt="" proto""="" src="./Images/Figma/Prototype.png"/></a> <a class="glightbox" href="./Images/Figma/Prototype.png"><img alt="" de="" fait="" figma""="" l'app="" protype="" src="./Images/Figma/Prototype.png" sur=""/></a>
<figcaption>"Proto"</figcaption> <figcaption>"Protype de l'app fait sur Figma"</figcaption>
</figure> </figure>
<h3 id="cas-dutilisation">Cas d'utilisation</h3> <h3 id="cas-dutilisation">Cas d'utilisation</h3>
<hr/> <hr/>
@@ -1137,21 +1160,21 @@ La raison la plus probable étant qu'Amazon avec son service AWS propose exactem
<p>Pour rappel, Amazon héberge directement le site de la F1TV et possède les droits sur les données de la F1. C'est sous le nom de AWS (le service d'hébergement d'Amazon) que la firme apparait en tant que sponsor.</p> <p>Pour rappel, Amazon héberge directement le site de la F1TV et possède les droits sur les données de la F1. C'est sous le nom de AWS (le service d'hébergement d'Amazon) que la firme apparait en tant que sponsor.</p>
<p>On peut voir ce nom apparaître assez souvent quand on regarde un Grand Prix car comme ils ont la main-mise sur les données ils peuvent insèrer des bandeaux d'informations sur le flux public sur ce qu'il se passe voir même faire des prédictions (Bien qu'un peu bancales)</p> <p>On peut voir ce nom apparaître assez souvent quand on regarde un Grand Prix car comme ils ont la main-mise sur les données ils peuvent insèrer des bandeaux d'informations sur le flux public sur ce qu'il se passe voir même faire des prédictions (Bien qu'un peu bancales)</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/AWSExample1.jpg"><img 1""="" alt="" aws="" example="" src="./Images/Screens/AWSExample1.jpg"/></a> <a class="glightbox" href="./Images/Screens/AWSExample1.jpg"><img alt="" aws="" en="" exemple="" gp""="" insertion="" src="./Images/Screens/AWSExample1.jpg"/></a>
<figcaption>"AWS example 1"</figcaption> <figcaption>"Exemple insertion AWS en GP"</figcaption>
</figure> </figure>
<p>Ce service s'appelle F1 Insights (Oui c'est un meilleur nom de projet que F1 Companion mais bon) et c'est, je pense, la raison pour laquelle on ne voit aucune API publique qui permette de correctement se renseigner en donées en direct pendant un Grand Prix. Ils ont du dégotter un juteux contrat pour s'occuper de toute l'infrastructure digitale de la F1 (du moins publique) en échange d'une exclusivité totale sur certaines choses comme les Data</p> <p>Ce service s'appelle F1 Insights (Oui c'est un meilleur nom de projet que F1 Companion mais bon) et c'est, je pense, la raison pour laquelle on ne voit aucune API publique qui permette de correctement se renseigner en donées en direct pendant un Grand Prix. Ils ont du dégotter un juteux contrat pour s'occuper de toute l'infrastructure digitale de la F1 (du moins publique) en échange d'une exclusivité totale sur certaines choses comme les Data</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/AWSExample2.jpg"><img 2""="" alt="" aws="" example="" src="./Images/Screens/AWSExample2.jpg"/></a> <a class="glightbox" href="./Images/Screens/AWSExample2.jpg"><img alt="" d'aws""="" data="" exemple="" src="./Images/Screens/AWSExample2.jpg"/></a>
<figcaption>"AWS example 2"</figcaption> <figcaption>"Exemple data d'AWS"</figcaption>
</figure> </figure>
<p>Evidemment je ne fais que conjecturer et ce que j'ai dit n'est pas à prendre au pied de la lettre mais c'est une explication possible je pense de pourquoi il est si difficile de trouver des données sur la F1 facilement en temps réel.</p> <p>Evidemment je ne fais que conjecturer et ce que j'ai dit n'est pas à prendre au pied de la lettre mais c'est une explication possible je pense de pourquoi il est si difficile de trouver des données sur la F1 facilement en temps réel.</p>
<p>Il existe bien quelques API un peu bancales publiques, mais le problème c'est qu'elles ne sont vraiment pas suffisante et je ne peux pas leur faire confiance quand je commente. Ce qu'il m'aurait fallut c'est une API publique et officielle qui me permette d'être sur que les données sont les bonnes et qu'elles arrivent le plus vite possible.</p> <p>Il existe bien quelques API un peu bancales publiques, mais le problème c'est qu'elles ne sont vraiment pas suffisante et je ne peux pas leur faire confiance quand je commente. Ce qu'il m'aurait fallut c'est une API publique et officielle qui me permette d'être sur que les données sont les bonnes et qu'elles arrivent le plus vite possible.</p>
<p>On pourrait croire que c'est impossible car cela n'existe pas comme je l'ai dit MAIS ! Ce n'est pas complêtement vrai. En effet depuis que je possède un abonnement à la F1TV, il existe une source d'informations très précieuse qui m'aide énormément dans mon quotidien de commentateur de Formule 1. La "DATA CHANNEL".</p> <p>On pourrait croire que c'est impossible car cela n'existe pas comme je l'ai dit MAIS ! Ce n'est pas complêtement vrai. En effet depuis que je possède un abonnement à la F1TV, il existe une source d'informations très précieuse qui m'aide énormément dans mon quotidien de commentateur de Formule 1. La "DATA CHANNEL".</p>
<p>La Data Channel est une page de la F1TV qui permet, pour chaque Grand Prix, de visualiser, sous la forme d'un flux vidéo, différentes informations capitales sur la course.</p> <p>La Data Channel est une page de la F1TV qui permet, pour chaque Grand Prix, de visualiser, sous la forme d'un flux vidéo, différentes informations capitales sur la course.</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/ScreenF1TvData.png"><img alt="" channel="" data="" example""="" src="./Images/Screens/ScreenF1TvData.png"/></a> <a class="glightbox" href="./Images/Screens/ScreenF1TvData.png"><img alt="" channel""="" data="" de="" exemple="" src="./Images/Screens/ScreenF1TvData.png"/></a>
<figcaption>"Data channel example"</figcaption> <figcaption>"Exemple de Data Channel"</figcaption>
</figure> </figure>
<p>Le problème, c'est que comme je viens de le dire, ces données ne sont pas accessibles comme un tableau HTML ou un flux RSS ou un tableau JSON. C'est un flux vidéo. <p>Le problème, c'est que comme je viens de le dire, ces données ne sont pas accessibles comme un tableau HTML ou un flux RSS ou un tableau JSON. C'est un flux vidéo.
Il faut savoir qu'entretenir une diffusion de flux vidéo en 1080P pendant deux heures accessible par des milliers d'abonnés est EXTRÊMENT cher surtout quand on le compare à simplement afficher les données dans un tableau. Ce qui veut dire que ce choix est délibéré et a un sens au niveau économique. Il faut savoir qu'entretenir une diffusion de flux vidéo en 1080P pendant deux heures accessible par des milliers d'abonnés est EXTRÊMENT cher surtout quand on le compare à simplement afficher les données dans un tableau. Ce qui veut dire que ce choix est délibéré et a un sens au niveau économique.
@@ -1192,8 +1215,8 @@ Je pense donc que c'est justement pour éviter que des petits malins puissent si
<p>Cependant Firefox de pas sa nature Open Source utilise "OpenH264" pour lire ces mêmes flux soumis à des DRM et OpenH264 n'implémente pas les mêmes restrictions.</p> <p>Cependant Firefox de pas sa nature Open Source utilise "OpenH264" pour lire ces mêmes flux soumis à des DRM et OpenH264 n'implémente pas les mêmes restrictions.</p>
<p>Sauf que Firefox n'est pas aussi facilement émulé que chrome et cela réduit notre choix de librairies à ... Une seule... Qui est Selenium. (Il existe aussi Pupetteer C# mais j'ai rencontré énormément de soucis avec cette dernière dès que je voulais lancer une vidéo)</p> <p>Sauf que Firefox n'est pas aussi facilement émulé que chrome et cela réduit notre choix de librairies à ... Une seule... Qui est Selenium. (Il existe aussi Pupetteer C# mais j'ai rencontré énormément de soucis avec cette dernière dès que je voulais lancer une vidéo)</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/FirefoxLogo.png"><img alt="" developper="" firefox="" logo""="" src="./Images/Screens/FirefoxLogo.png"/></a> <a class="glightbox" href="./Images/Screens/FirefoxLogo.png"><img alt="" dev="" firefox="" logo""="" src="./Images/Screens/FirefoxLogo.png"/></a>
<figcaption>"Firefox Developper logo"</figcaption> <figcaption>"Firefox dev logo"</figcaption>
</figure> </figure>
<p>{: style="height:150px;width:150px"}</p> <p>{: style="height:150px;width:150px"}</p>
<p>Mais même si la documentation est plutôt maigre parfois, c'est une bonne librairie qui permet de très bien contrôler une instance de chrome ou de Firefox.</p> <p>Mais même si la documentation est plutôt maigre parfois, c'est une bonne librairie qui permet de très bien contrôler une instance de chrome ou de Firefox.</p>
@@ -1260,8 +1283,8 @@ fullScreenButton.Click();
<p>Si on regarde de loin on peut se dire que la structure est plutôt simple mais c'est loin d'être le cas. <p>Si on regarde de loin on peut se dire que la structure est plutôt simple mais c'est loin d'être le cas.
On peut y voir au moins 4 zones contenant de l'information dans un format différent.</p> On peut y voir au moins 4 zones contenant de l'information dans un format différent.</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Figma/WindowZoneExplanation1.png"><img alt="" main="" src="./Images/Figma/WindowZoneExplanation1.png" zones""=""/></a> <a class="glightbox" href="./Images/Figma/WindowZoneExplanation1.png"><img alt="" principales""="" src="./Images/Figma/WindowZoneExplanation1.png" zones=""/></a>
<figcaption>"Main zones"</figcaption> <figcaption>"Zones principales"</figcaption>
</figure> </figure>
<p>Dans l'exemple ci dessus on peut voir 3 zones mais on aurait également pu comprendre la zone de position des pilotes autour du circuit pour faire 4.</p> <p>Dans l'exemple ci dessus on peut voir 3 zones mais on aurait également pu comprendre la zone de position des pilotes autour du circuit pour faire 4.</p>
<p>Ces 4 zones sont très différentes et contiennent d'autres informations. Pour ce travail de diplôme je ne m'occupe que de la zone principale. Mais je pense que le titre et les infos de circuit ne prendrait pas tant de temps que ca à implémenter.</p> <p>Ces 4 zones sont très différentes et contiennent d'autres informations. Pour ce travail de diplôme je ne m'occupe que de la zone principale. Mais je pense que le titre et les infos de circuit ne prendrait pas tant de temps que ca à implémenter.</p>
@@ -1289,39 +1312,39 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé
</ul> </ul>
<p>Voila donc un petit diagramme qui montre le découpage du programme :</p> <p>Voila donc un petit diagramme qui montre le découpage du programme :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Figma/WindowZoneExplanationDiagram.png"><img alt="" diagramme="" src="./Images/Figma/WindowZoneExplanationDiagram.png" zones""=""/></a> <a class="glightbox" href="./Images/Figma/WindowZoneExplanationDiagram.png"><img alt="" de="" des="" diagramme="" explicatif="" l'architecture="" src="./Images/Figma/WindowZoneExplanationDiagram.png" zones""=""/></a>
<figcaption>"Diagramme zones"</figcaption> <figcaption>"Diagramme explicatif de l'architecture des zones"</figcaption>
</figure> </figure>
<p>Pour visualiser encore un peu mieux comment ce découpage prend forme voici ce que chaque zone et Window contient.</p> <p>Pour visualiser encore un peu mieux comment ce découpage prend forme voici ce que chaque zone et Window contient.</p>
<p>Main Zone :</p> <p>Main Zone :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/MainZoneExample.png"><img alt="" main="" src="./Images/Screens/MainZoneExample.png" zone""=""/></a> <a class="glightbox" href="./Images/Screens/MainZoneExample.png"><img alt="" exemple="" principale""="" src="./Images/Screens/MainZoneExample.png" zone=""/></a>
<figcaption>"Main zone"</figcaption> <figcaption>"Exemple zone principale"</figcaption>
</figure> </figure>
<p>Driver Zone :</p> <p>Driver Zone :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/DriverZoneExample.png"><img alt="" driver="" src="./Images/Screens/DriverZoneExample.png" zone""=""/></a> <a class="glightbox" href="./Images/Screens/DriverZoneExample.png"><img alt="" de="" exemple="" pilote""="" src="./Images/Screens/DriverZoneExample.png" zone=""/></a>
<figcaption>"Driver zone"</figcaption> <figcaption>"Exemple zone de pilote"</figcaption>
</figure> </figure>
<p>Driver Position Window :</p> <p>Driver Position Window :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/DriverPositionExample.png"><img alt="" driver="" position="" src="./Images/Screens/DriverPositionExample.png" window""=""/></a> <a class="glightbox" href="./Images/Screens/DriverPositionExample.png"><img alt="" de="" exemple="" fenêtre="" position""="" src="./Images/Screens/DriverPositionExample.png"/></a>
<figcaption>"Driver position Window"</figcaption> <figcaption>"Exemple de fenêtre de position"</figcaption>
</figure> </figure>
<p>Driver name Window :</p> <p>Driver name Window :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/DriverNameExample.png"><img alt="" driver="" name="" src="./Images/Screens/DriverNameExample.png" window""=""/></a> <a class="glightbox" href="./Images/Screens/DriverNameExample.png"><img alt="" de="" exemple="" fenêtre="" nom""="" src="./Images/Screens/DriverNameExample.png"/></a>
<figcaption>"Driver name window"</figcaption> <figcaption>"Exemple de fenêtre de nom"</figcaption>
</figure> </figure>
<p>Driver LapTime Window :</p> <p>Driver LapTime Window :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/DriverLapTimeExample.png"><img alt="" driver="" laptime="" src="./Images/Screens/DriverLapTimeExample.png" window""=""/></a> <a class="glightbox" href="./Images/Screens/DriverLapTimeExample.png"><img alt="" au="" de="" exemple="" fenêtre="" src="./Images/Screens/DriverLapTimeExample.png" temps="" tour""=""/></a>
<figcaption>"Driver Laptime window"</figcaption> <figcaption>"Exemple de fenêtre de temps au tour"</figcaption>
</figure> </figure>
<p>Driver Tyre Window :</p> <p>Driver Tyre Window :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/DriverTyreWindowExample.png"><img alt="" driver="" src="./Images/Screens/DriverTyreWindowExample.png" tyre="" window""=""/></a> <a class="glightbox" href="./Images/Screens/DriverTyreWindowExample.png"><img alt="" de="" exemple="" fenêtre="" pneus""="" src="./Images/Screens/DriverTyreWindowExample.png"/></a>
<figcaption>"Driver tyre window"</figcaption> <figcaption>"Exemple de fenêtre pneus"</figcaption>
</figure> </figure>
<p>Il existe d'autres types de Window mais ce sont les principaux.</p> <p>Il existe d'autres types de Window mais ce sont les principaux.</p>
<p>On se rend assez facilement compte que chacunes de ces windows va avoir besoin d'un traitement spécifique car la manière de reconnaitre le pneu utilisé et le temps au tour ne peut pas être la même.</p> <p>On se rend assez facilement compte que chacunes de ces windows va avoir besoin d'un traitement spécifique car la manière de reconnaitre le pneu utilisé et le temps au tour ne peut pas être la même.</p>
@@ -1338,29 +1361,29 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé
<p>Exemple :</p> <p>Exemple :</p>
<p>Prenons le chiffre 9. Dans l'image il peut être représenté de cette manière :</p> <p>Prenons le chiffre 9. Dans l'image il peut être représenté de cette manière :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Figma/Bad9Example.png"><img alt="" bad9exemple""="" src="./Images/Figma/Bad9Example.png"/></a> <a class="glightbox" href="./Images/Figma/Bad9Example.png"><img alt="" avant="" chiffre="" de="" exemple="" post="" src="./Images/Figma/Bad9Example.png" traitement""=""/></a>
<figcaption>"Bad9Exemple"</figcaption> <figcaption>"Exemple de chiffre avant post traitement"</figcaption>
</figure> </figure>
<p>On peut voir qu'il est flou, pour nous cela ne pose pas de problème et je pense que à peu près nimporte qui peut dire que c'est un 9.</p> <p>On peut voir qu'il est flou, pour nous cela ne pose pas de problème et je pense que à peu près nimporte qui peut dire que c'est un 9.</p>
<p>Cependant comme les contours sont flous et même si on essaie de retirer le background :</p> <p>Cependant comme les contours sont flous et même si on essaie de retirer le background :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Figma/Aliased9.png"><img 9""="" aliased="" alt="" src="./Images/Figma/Aliased9.png"/></a> <a class="glightbox" href="./Images/Figma/Aliased9.png"><img 9="" aliasing""="" alt="" anti="" avec="" src="./Images/Figma/Aliased9.png"/></a>
<figcaption>"Aliased 9"</figcaption> <figcaption>"9 avec anti aliasing"</figcaption>
</figure> </figure>
<p>On voit que le 9 n'est pas clairement définit. En effet on pourrait le comprendre comme :</p> <p>On voit que le 9 n'est pas clairement définit. En effet on pourrait le comprendre comme :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Figma/Horrible9.png"><img alt="" contour""="" first="" src="./Images/Figma/Horrible9.png"/></a> <a class="glightbox" href="./Images/Figma/Horrible9.png"><img alt="" contours""="" de="" exemple="" premier="" src="./Images/Figma/Horrible9.png"/></a>
<figcaption>"First contour"</figcaption> <figcaption>"Premier exemple de contours"</figcaption>
</figure> </figure>
<p>Ou comme :</p> <p>Ou comme :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Figma/Clean9Example.png"><img alt="" contour""="" second="" src="./Images/Figma/Clean9Example.png"/></a> <a class="glightbox" href="./Images/Figma/Clean9Example.png"><img alt="" contours""="" de="" exemple="" second="" src="./Images/Figma/Clean9Example.png"/></a>
<figcaption>"Second contour"</figcaption> <figcaption>"Second exemple de contours"</figcaption>
</figure> </figure>
<p>Voire même simplement comme :</p> <p>Voire même simplement comme :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Figma/Filled9.png"><img alt="" big="" contour""="" src="./Images/Figma/Filled9.png"/></a> <a class="glightbox" href="./Images/Figma/Filled9.png"><img alt="" coutour="" de="" exemple="" généreux""="" src="./Images/Figma/Filled9.png"/></a>
<figcaption>"Big contour"</figcaption> <figcaption>"Exemple de coutour généreux"</figcaption>
</figure> </figure>
<p>Et on se rend bien compte que les performances de detection ne sont pas les mêmes dans ces trois cas.</p> <p>Et on se rend bien compte que les performances de detection ne sont pas les mêmes dans ces trois cas.</p>
<p>Il faut donc faire un certain post traitement des images pour supprimer les éléments parasites, les couleurs, et augmenter la visibilité des contours importants.</p> <p>Il faut donc faire un certain post traitement des images pour supprimer les éléments parasites, les couleurs, et augmenter la visibilité des contours importants.</p>
@@ -1372,8 +1395,8 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé
<p>Cette reconnaissance concerne donc des lettres qui font des mots ou des noms.</p> <p>Cette reconnaissance concerne donc des lettres qui font des mots ou des noms.</p>
<p>Voici un exemple de la WINDOW nom de pilote en entrée :</p> <p>Voici un exemple de la WINDOW nom de pilote en entrée :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/ExempleTextRaw.png"><img alt="" exemple="" raw""="" src="./Images/Screens/ExempleTextRaw.png"/></a> <a class="glightbox" href="./Images/Screens/ExempleTextRaw.png"><img alt="" cru""="" exemple="" src="./Images/Screens/ExempleTextRaw.png" texte=""/></a>
<figcaption>"Exemple raw"</figcaption> <figcaption>"Exemple texte cru"</figcaption>
</figure> </figure>
<p>Ce texte peut paraitre bon, cependant quand on le lance dans Tesseract, il ne va pas toujours donner un résultat parfait. Il faut aussi savoir qu'il y a des noms pas mal plus pénibles que Tesseract a plus de mal à reconnaitres, soit à cause des lettres utilisées, soit car le nom est un nom d'une autre région et qui ne veut rien dire en anglais ce qui empêche l'utilisation de dictionnaire (Ex : Tsunoda est un nom japonais et parfois il est difficile pour Tesseract de le reconnaitre car si une lettre pose problême il ne peut pas trouver de contexte qui puisse l'aider).</p> <p>Ce texte peut paraitre bon, cependant quand on le lance dans Tesseract, il ne va pas toujours donner un résultat parfait. Il faut aussi savoir qu'il y a des noms pas mal plus pénibles que Tesseract a plus de mal à reconnaitres, soit à cause des lettres utilisées, soit car le nom est un nom d'une autre région et qui ne veut rien dire en anglais ce qui empêche l'utilisation de dictionnaire (Ex : Tsunoda est un nom japonais et parfois il est difficile pour Tesseract de le reconnaitre car si une lettre pose problême il ne peut pas trouver de contexte qui puisse l'aider).</p>
<p>Donc pour le rendre plus facilement lisible et augmenter les chances que toutes les lettres soient découvertes, voici les étapes que j'ai mis en place.</p> <p>Donc pour le rendre plus facilement lisible et augmenter les chances que toutes les lettres soient découvertes, voici les étapes que j'ai mis en place.</p>
@@ -1384,18 +1407,18 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé
</figure> </figure>
<p>2 : Je fais un <em>Treshhold</em> de 165 car avec moins le texte parfois prend trop du background et avec plus les lettres sont trop fines.</p> <p>2 : Je fais un <em>Treshhold</em> de 165 car avec moins le texte parfois prend trop du background et avec plus les lettres sont trop fines.</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/ExempleTextTresholding.png"><img alt="" src="./Images/Screens/ExempleTextTresholding.png" treshold""=""/></a> <a class="glightbox" href="./Images/Screens/ExempleTextTresholding.png"><img alt="" après="" src="./Images/Screens/ExempleTextTresholding.png" texte="" treshold""=""/></a>
<figcaption>"Treshold"</figcaption> <figcaption>"Texte après Treshold"</figcaption>
</figure> </figure>
<p>3 : Je fais un <em>Resize</em> de l'image pour avoir une meilleure résolution et permettre une meilleure détection. J'augmente la hauteur et la largeur par un facteur 2. J'ai trouvé cette valeur suffisante et aller plus haut consomme beaucoup de ressources.</p> <p>3 : Je fais un <em>Resize</em> de l'image pour avoir une meilleure résolution et permettre une meilleure détection. J'augmente la hauteur et la largeur par un facteur 2. J'ai trouvé cette valeur suffisante et aller plus haut consomme beaucoup de ressources.</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/ExempleTextResize.png"><img alt="" resize""="" src="./Images/Screens/ExempleTextResize.png"/></a> <a class="glightbox" href="./Images/Screens/ExempleTextResize.png"><img alt="" après="" resize""="" src="./Images/Screens/ExempleTextResize.png" texte=""/></a>
<figcaption>"Resize"</figcaption> <figcaption>"Texte après Resize"</figcaption>
</figure> </figure>
<p>4: Je fais une très rapide <em>Dilatation</em> du texte pour retirer le flou amené par la methode de Resize. Je n'utilise qu'une valeur de 1 car je ne veux pas trop changer comment le texte est modelé je veux juste retirer le flou.</p> <p>4: Je fais une très rapide <em>Dilatation</em> du texte pour retirer le flou amené par la methode de Resize. Je n'utilise qu'une valeur de 1 car je ne veux pas trop changer comment le texte est modelé je veux juste retirer le flou.</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/ExempleTextDilatation.png"><img alt="" dilatation""="" src="./Images/Screens/ExempleTextDilatation.png"/></a> <a class="glightbox" href="./Images/Screens/ExempleTextDilatation.png"><img alt="" après="" dilatation""="" src="./Images/Screens/ExempleTextDilatation.png" texte=""/></a>
<figcaption>"Dilatation"</figcaption> <figcaption>"Texte après Dilatation"</figcaption>
</figure> </figure>
<p><em>Explication des methodes précises plus bas</em></p> <p><em>Explication des methodes précises plus bas</em></p>
<p>Voila pour ce qui est du post processing. Je ne dis pas que ce sont les meilleurs paramêtres possibles mais dans mes tests ce sont ceux qui ont le mieux marchés.</p> <p>Voila pour ce qui est du post processing. Je ne dis pas que ce sont les meilleurs paramêtres possibles mais dans mes tests ce sont ceux qui ont le mieux marchés.</p>
@@ -1424,36 +1447,36 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé
<p>Cependant, tous ces temps possèdent le même type de post-traitement avant d'être envoyés à Tesseract.</p> <p>Cependant, tous ces temps possèdent le même type de post-traitement avant d'être envoyés à Tesseract.</p>
<p>Voici un exemple de temps au tour avant toute transformation :</p> <p>Voici un exemple de temps au tour avant toute transformation :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/ExempleLapTimeBefore.png"><img alt="" lap="" src="./Images/Screens/ExempleLapTimeBefore.png" time""=""/></a> <a class="glightbox" href="./Images/Screens/ExempleLapTimeBefore.png"><img alt="" au="" avant="" src="./Images/Screens/ExempleLapTimeBefore.png" temps="" tour="" traitement""=""/></a>
<figcaption>"Lap time"</figcaption> <figcaption>"Temps au tour avant traitement"</figcaption>
</figure> </figure>
<p>On peut avoir l'impression que ce texte est tout à fait lisible et facile à décoder surtout quand on le voit de loin comme ca. Cependant, il faut imaginer que ces chiffres font 13 pixels de haut en comptant le flou et comme expliqué plus haut ce flou dans ces echelles est terrible.</p> <p>On peut avoir l'impression que ce texte est tout à fait lisible et facile à décoder surtout quand on le voit de loin comme ca. Cependant, il faut imaginer que ces chiffres font 13 pixels de haut en comptant le flou et comme expliqué plus haut ce flou dans ces echelles est terrible.</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/ExempleLapTimeBeforeUpscaled.png"><img alt="" lap="" src="./Images/Screens/ExempleLapTimeBeforeUpscaled.png" time""=""/></a> <a class="glightbox" href="./Images/Screens/ExempleLapTimeBeforeUpscaled.png"><img alt="" au="" src="./Images/Screens/ExempleLapTimeBeforeUpscaled.png" temps="" tour="" zoomé""=""/></a>
<figcaption>"Lap time"</figcaption> <figcaption>"Temps au tour zoomé"</figcaption>
</figure> </figure>
<p>Si on donne cette image à Tesseract, les '3' deviennent des '9', des '9' deviennent des '8', des '2' deviennent eux aussi des '9', le tout parfois inversement et de manière complêtement imprévisible. Ca n'est simplement pas utilisable.</p> <p>Si on donne cette image à Tesseract, les '3' deviennent des '9', des '9' deviennent des '8', des '2' deviennent eux aussi des '9', le tout parfois inversement et de manière complêtement imprévisible. Ca n'est simplement pas utilisable.</p>
<p>Cette partie est un peu plus complexe car si la detection n'est pas fiable les chiffres sont simplement inutilisables. Si à tout moment un temps au tour de 1:39.106 devient 1:32.108 c'est juste pas possible.</p> <p>Cette partie est un peu plus complexe car si la detection n'est pas fiable les chiffres sont simplement inutilisables. Si à tout moment un temps au tour de 1:39.106 devient 1:32.108 c'est juste pas possible.</p>
<p>Voici donc les étapes de post-traitement que j'ai mis en place pour leur détection :</p> <p>Voici donc les étapes de post-traitement que j'ai mis en place pour leur détection :</p>
<p>1: J'applique un <em>Treshold</em> de 185 pour enlever les ambiguités d'alisaising et avoir une image en noir et blanc claire. La valeur de 185 est assez élevée car le but est de vraiment garder uniquement les contours. Comme les chiffres se ressemlent beaucoup plu que les lettres, il faut tenter le plus possible de conserver leur formes spécifiques. Je me suis rendu compte que cette valeur était une de celles qui marchent le mieux.</p> <p>1: J'applique un <em>Treshold</em> de 185 pour enlever les ambiguités d'alisaising et avoir une image en noir et blanc claire. La valeur de 185 est assez élevée car le but est de vraiment garder uniquement les contours. Comme les chiffres se ressemlent beaucoup plu que les lettres, il faut tenter le plus possible de conserver leur formes spécifiques. Je me suis rendu compte que cette valeur était une de celles qui marchent le mieux.</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/ExempleLapTimeTreshold.png"><img alt="" src="./Images/Screens/ExempleLapTimeTreshold.png" treshold""=""/></a> <a class="glightbox" href="./Images/Screens/ExempleLapTimeTreshold.png"><img alt="" après="" au="" src="./Images/Screens/ExempleLapTimeTreshold.png" temps="" tour="" treshold""=""/></a>
<figcaption>"Treshold"</figcaption> <figcaption>"Temps au tour après Treshold"</figcaption>
</figure> </figure>
<p>2: J'applique un <em>Resize</em> de 2 pour augmenter la résolution des chiffres et permettre une meilleure détection. Le but est d'avoir plus de pixels et donc de permettre à Tesseract de mieux utiliser ses matrices de convolution.</p> <p>2: J'applique un <em>Resize</em> de 2 pour augmenter la résolution des chiffres et permettre une meilleure détection. Le but est d'avoir plus de pixels et donc de permettre à Tesseract de mieux utiliser ses matrices de convolution.</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/ExempleLapTimeResize.png"><img alt="" resize""="" src="./Images/Screens/ExempleLapTimeResize.png"/></a> <a class="glightbox" href="./Images/Screens/ExempleLapTimeResize.png"><img alt="" après="" au="" resize""="" src="./Images/Screens/ExempleLapTimeResize.png" temps="" tour=""/></a>
<figcaption>"Resize"</figcaption> <figcaption>"Temps au tour après Resize"</figcaption>
</figure> </figure>
<p>3: Comme le <em>Resize</em> amène du flou, j'utilise une methode de Dilatation<em> qui me permet de retirer ce flou et de remplir un peu plus certaines parties qui ont été un peu laissée par le </em>Resize*;</p> <p>3: Comme le <em>Resize</em> amène du flou, j'utilise une methode de <em>Dilatation</em> qui me permet de retirer ce flou et de remplir un peu plus certaines parties qui ont été un peu laissée par le <em>Resize</em>;</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/ExempleLapTimeDilatation.png"><img alt="" dilatation""="" src="./Images/Screens/ExempleLapTimeDilatation.png"/></a> <a class="glightbox" href="./Images/Screens/ExempleLapTimeDilatation.png"><img alt="" après="" au="" dilatation""="" src="./Images/Screens/ExempleLapTimeDilatation.png" temps="" tour=""/></a>
<figcaption>"Dilatation"</figcaption> <figcaption>"Temps au tour après Dilatation"</figcaption>
</figure> </figure>
<p>4: Contrairement aux mots plus haut, la rondeur ajoutée par la dilatation n'est pas vraiment désirée. En effet, elle peut rendre confuse certains chiffres et empêcher Tesseract de bien trouver le chiffre. Alors j'applique une <em>Erosion</em> qui me permet de contrecarrer en partie les rondeurs ajoutées par la dilatation et retrouver des chiffres bien formées. Pour l'<em>Erosion</em> et la <em>Dilatation</em> j'ai utilisé une valeur de 1 car je ne voulais pas trop changer les chiffres.</p> <p>4: Contrairement aux mots plus haut, la rondeur ajoutée par la dilatation n'est pas vraiment désirée. En effet, elle peut rendre confuse certains chiffres et empêcher Tesseract de bien trouver le chiffre. Alors j'applique une <em>Erosion</em> qui me permet de contrecarrer en partie les rondeurs ajoutées par la dilatation et retrouver des chiffres bien formées. Pour l'<em>Erosion</em> et la <em>Dilatation</em> j'ai utilisé une valeur de 1 car je ne voulais pas trop changer les chiffres.</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/ExempleLapTimeErode.png"><img alt="" erode""="" src="./Images/Screens/ExempleLapTimeErode.png"/></a> <a class="glightbox" href="./Images/Screens/ExempleLapTimeErode.png"><img alt="" après="" au="" erosion""="" src="./Images/Screens/ExempleLapTimeErode.png" temps="" tour=""/></a>
<figcaption>"Erode"</figcaption> <figcaption>"Temps au tour après Erosion"</figcaption>
</figure> </figure>
<p><em>Explication des methodes précises plus bas</em></p> <p><em>Explication des methodes précises plus bas</em></p>
<p>Et avec ce post processing on retrouve de plutôts bon résultats qui demandent peu de traitement.</p> <p>Et avec ce post processing on retrouve de plutôts bon résultats qui demandent peu de traitement.</p>
@@ -1485,31 +1508,31 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé
<li>Les pneus pluie</li> <li>Les pneus pluie</li>
</ul> </ul>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Photos/Tyres.png"><img alt="" src="./Images/Photos/Tyres.png" tyres""=""/></a> <a class="glightbox" href="./Images/Photos/Tyres.png"><img alt="" de="" gamme="" pirelli""="" pneus="" src="./Images/Photos/Tyres.png"/></a>
<figcaption>"Tyres"</figcaption> <figcaption>"Gamme de pneus Pirelli"</figcaption>
</figure> </figure>
<p>Les trois premiers pneus sont des pneus faits pour piste sèche, le pneu intermédiaire pour piste humide et le neu pluie pour la pluie.</p> <p>Les trois premiers pneus sont des pneus faits pour piste sèche, le pneu intermédiaire pour piste humide et le neu pluie pour la pluie.</p>
<p>Chaque pneu a sa durée de vie et son niveau de performance propre mais je ne vais pas rentrer dans le détail ici. Tout ce qu'il faut savoir ce que savoir sur quel pneu chaque pilote est et depuis combien de temps il les chausse est une information très importante.</p> <p>Chaque pneu a sa durée de vie et son niveau de performance propre mais je ne vais pas rentrer dans le détail ici. Tout ce qu'il faut savoir ce que savoir sur quel pneu chaque pilote est et depuis combien de temps il les chausse est une information très importante.</p>
<p>Chaque pneu a une couleur donnée qui permet de les différencier.</p> <p>Chaque pneu a une couleur donnée qui permet de les différencier.</p>
<p>Voici un exemple de ce à quoi une WINDOW de pneus peut ressembler :</p> <p>Voici un exemple de ce à quoi une WINDOW de pneus peut ressembler :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/TyreZoneExemple3.png"><img 1""="" alt="" exemple="" src="./Images/Screens/TyreZoneExemple3.png"/></a> <a class="glightbox" href="./Images/Screens/TyreZoneExemple3.png"><img 1""="" alt="" exemple="" pneus="" src="./Images/Screens/TyreZoneExemple3.png" zone=""/></a>
<figcaption>"Exemple 1"</figcaption> <figcaption>"Exemple zone pneus 1"</figcaption>
</figure> </figure>
<p>Mais cette zone peut aussi ressembler à ca :</p> <p>Mais cette zone peut aussi ressembler à ca :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/TyreZoneExemple2.png"><img 2""="" alt="" exemple="" src="./Images/Screens/TyreZoneExemple2.png"/></a> <a class="glightbox" href="./Images/Screens/TyreZoneExemple2.png"><img 2""="" alt="" exemple="" pneus="" src="./Images/Screens/TyreZoneExemple2.png" zone=""/></a>
<figcaption>"Exemple 2"</figcaption> <figcaption>"Exemple zone pneus 2"</figcaption>
</figure> </figure>
<p>Mais aussi à ca :</p> <p>Mais aussi à ca :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/TyreZoneExemple1.png"><img 3""="" alt="" exemple="" src="./Images/Screens/TyreZoneExemple1.png"/></a> <a class="glightbox" href="./Images/Screens/TyreZoneExemple1.png"><img 3""="" alt="" exemple="" pneus="" src="./Images/Screens/TyreZoneExemple1.png" zone=""/></a>
<figcaption>"Exemple 3"</figcaption> <figcaption>"Exemple zone pneus 3"</figcaption>
</figure> </figure>
<p>Voire même ca :</p> <p>Voire même ca :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/TyreZoneExemple4.png"><img 4""="" alt="" exemple="" src="./Images/Screens/TyreZoneExemple4.png"/></a> <a class="glightbox" href="./Images/Screens/TyreZoneExemple4.png"><img 4""="" alt="" exemple="" pneus="" src="./Images/Screens/TyreZoneExemple4.png" zone=""/></a>
<figcaption>"Exemple 4"</figcaption> <figcaption>"Exemple zone pneus 4"</figcaption>
</figure> </figure>
<p>Je pense que vous pouvez tout de suite comprendre la difficulté que représente la tâche de récupèration de données à partir de cette image.</p> <p>Je pense que vous pouvez tout de suite comprendre la difficulté que représente la tâche de récupèration de données à partir de cette image.</p>
<p>En gros le fonctionnement de cette zone d'information est assez simple.</p> <p>En gros le fonctionnement de cette zone d'information est assez simple.</p>
@@ -1526,13 +1549,13 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé
<p>Ensuite après avoir trouvé le premier obstacle, je récupère une zone qui doit englober le cercle.</p> <p>Ensuite après avoir trouvé le premier obstacle, je récupère une zone qui doit englober le cercle.</p>
<p>Voici un exemple avec cette image en entrée :</p> <p>Voici un exemple avec cette image en entrée :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/TyreZoneFull.png"><img alt="" full="" src="./Images/Screens/TyreZoneFull.png" zone""=""/></a> <a class="glightbox" href="./Images/Screens/TyreZoneFull.png"><img alt="" complête""="" src="./Images/Screens/TyreZoneFull.png" zone=""/></a>
<figcaption>"Full zone"</figcaption> <figcaption>"Zone complête"</figcaption>
</figure> </figure>
<p>Elle est automatiquement coupée de cette facon :</p> <p>Elle est automatiquement coupée de cette facon :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/TyreZoneCropped.png"><img alt="" cropped="" src="./Images/Screens/TyreZoneCropped.png" zone""=""/></a> <a class="glightbox" href="./Images/Screens/TyreZoneCropped.png"><img alt="" automatiquement""="" coupée="" src="./Images/Screens/TyreZoneCropped.png" zone=""/></a>
<figcaption>"Cropped zone"</figcaption> <figcaption>"Zone coupée automatiquement"</figcaption>
</figure> </figure>
<p>Cela me permet d'isoler uniquement ce qui m'intéresse ce qui est très pratique pour Tesseract et pour la detection de couleur.</p> <p>Cela me permet d'isoler uniquement ce qui m'intéresse ce qui est très pratique pour Tesseract et pour la detection de couleur.</p>
<p>Ensuite avec cette image je peux commencer le processus de reconnaissance.</p> <p>Ensuite avec cette image je peux commencer le processus de reconnaissance.</p>
@@ -1541,38 +1564,38 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé
<p>Il y a cinq couleurs des pneus possibles :</p> <p>Il y a cinq couleurs des pneus possibles :</p>
<p>"#ff0000" pneu tendre/soft</p> <p>"#ff0000" pneu tendre/soft</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Photos/SoftTyreColor.png"><img alt="" color""="" soft="" src="./Images/Photos/SoftTyreColor.png" tyre=""/></a> <a class="glightbox" href="./Images/Photos/SoftTyreColor.png"><img alt="" couleur="" d'un="" pneu="" src="./Images/Photos/SoftTyreColor.png" tendre""=""/></a>
<figcaption>"Soft tyre color"</figcaption> <figcaption>"Couleur d'un pneu tendre"</figcaption>
</figure> </figure>
<p>"#f5bf00" pneu medium</p> <p>"#f5bf00" pneu medium</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Photos/MediumTyreColor.png"><img alt="" color""="" medium="" src="./Images/Photos/MediumTyreColor.png" tyre=""/></a> <a class="glightbox" href="./Images/Photos/MediumTyreColor.png"><img alt="" couleur="" d'un="" medium""="" pneu="" src="./Images/Photos/MediumTyreColor.png"/></a>
<figcaption>"medium tyre color"</figcaption> <figcaption>"Couleur d'un pneu medium"</figcaption>
</figure> </figure>
<p>"#a4a5a8" pneu dur/hard</p> <p>"#a4a5a8" pneu dur/hard</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Photos/HardTyreColor.png"><img alt="" color""="" hard="" src="./Images/Photos/HardTyreColor.png" tyre=""/></a> <a class="glightbox" href="./Images/Photos/HardTyreColor.png"><img alt="" couleur="" d'un="" dur""="" pneu="" src="./Images/Photos/HardTyreColor.png"/></a>
<figcaption>"Hard tyre color"</figcaption> <figcaption>"Couleur d'un pneu dur"</figcaption>
</figure> </figure>
<p>"#00a42e" pneu inter</p> <p>"#00a42e" pneu inter</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Photos/IntermediateTyreColor.png"><img alt="" color""="" inter="" src="./Images/Photos/IntermediateTyreColor.png" tyre=""/></a> <a class="glightbox" href="./Images/Photos/IntermediateTyreColor.png"><img alt="" couleur="" d'un="" intermédiaire""="" pneu="" src="./Images/Photos/IntermediateTyreColor.png"/></a>
<figcaption>"Inter tyre color"</figcaption> <figcaption>"Couleur d'un pneu intermédiaire"</figcaption>
</figure> </figure>
<p>"#2760a6" pneu pluie/wet</p> <p>"#2760a6" pneu pluie/wet</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Photos/WetTyreColor.png"><img alt="" color""="" src="./Images/Photos/WetTyreColor.png" tyre="" wet=""/></a> <a class="glightbox" href="./Images/Photos/WetTyreColor.png"><img alt="" couleur="" d'un="" pluie""="" pneu="" src="./Images/Photos/WetTyreColor.png"/></a>
<figcaption>"Wet tyre color"</figcaption> <figcaption>"Couleur d'un pneu pluie"</figcaption>
</figure> </figure>
<p>Ce qui est pratique c'est que même dans les cas ou il n'y a pas beaucoup de couleur comme celui la :</p> <p>Ce qui est pratique c'est que même dans les cas ou il n'y a pas beaucoup de couleur comme celui la :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/TyreZoneHard.png"><img alt="" but="" hard="" letter""="" only="" src="./Images/Screens/TyreZoneHard.png" the="" tyre=""/></a> <a class="glightbox" href="./Images/Screens/TyreZoneHard.png"><img 0="" alt="" avec="" dur="" pneu="" src="./Images/Screens/TyreZoneHard.png" tours""=""/></a>
<figcaption>"Hard tyre but only the letter"</figcaption> <figcaption>"Pneu dur avec 0 tours"</figcaption>
</figure> </figure>
<p>On arrive à une couleur moyenne de :</p> <p>On arrive à une couleur moyenne de :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/TyreZoneHardAVG.png"><img above""="" alt="" average="" color="" from="" picture="" src="./Images/Screens/TyreZoneHardAVG.png" the=""/></a> <a class="glightbox" href="./Images/Screens/TyreZoneHardAVG.png"><img alt="" après="" background""="" ci="" couleur="" de="" dessus="" du="" l'image="" moyenne="" soustraction="" src="./Images/Screens/TyreZoneHardAVG.png"/></a>
<figcaption>"The average color from the picture above"</figcaption> <figcaption>"Couleur moyenne de l'image ci dessus après soustraction du background"</figcaption>
</figure> </figure>
<p>Et il est donc assez facile de determiner le type de pneu en question.</p> <p>Et il est donc assez facile de determiner le type de pneu en question.</p>
<p>Attention, les résultats peuvent être très vite dérangés par la couleur du pneu précédent si le découpage de la fenêtre n'a pas été assez précis.</p> <p>Attention, les résultats peuvent être très vite dérangés par la couleur du pneu précédent si le découpage de la fenêtre n'a pas été assez précis.</p>
@@ -1582,25 +1605,25 @@ On peut y voir au moins 4 zones contenant de l'information dans un format diffé
<p>Il faut donc retirer le background et ensuite la couleur. Sauf que comme le chiffre est de la couleur du background, si on retire le background et ensuite la couleur il ne reste plus rien. Il faut donc retirer le background AUTOUR du rond, et ensuite si on retire la couleur il devrait rester le chiffre sur fond blanc.</p> <p>Il faut donc retirer le background et ensuite la couleur. Sauf que comme le chiffre est de la couleur du background, si on retire le background et ensuite la couleur il ne reste plus rien. Il faut donc retirer le background AUTOUR du rond, et ensuite si on retire la couleur il devrait rester le chiffre sur fond blanc.</p>
<p>Pour se faire, j'ai tiré des traits depuis les bords de l'image jusqu'à ce qu'ils rencontrent le rond. Ensuite je retire tous les pixels entre le rond et les bords de l'image ce qui nous donne ceci :</p> <p>Pour se faire, j'ai tiré des traits depuis les bords de l'image jusqu'à ce qu'ils rencontrent le rond. Ensuite je retire tous les pixels entre le rond et les bords de l'image ce qui nous donne ceci :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/TyreZoneCropedWithoutBackGround.png"><img alt="" background""="" no="" outer="" src="./Images/Screens/TyreZoneCropedWithoutBackGround.png"/></a> <a class="glightbox" href="./Images/Screens/TyreZoneCropedWithoutBackGround.png"><img alt="" avec="" background="" en="" le="" moins""="" pneu="" src="./Images/Screens/TyreZoneCropedWithoutBackGround.png" zone=""/></a>
<figcaption>"No outer background"</figcaption> <figcaption>"Zone pneu avec le background en moins"</figcaption>
</figure> </figure>
<p>Ensuite on peu retirer les pixels qui ont une valeur dans un channel RGB plus haute qu'un certain seuil :</p> <p>Ensuite on peu retirer les pixels qui ont une valeur dans un channel RGB plus haute qu'un certain seuil :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/TyreZoneFilter1.png"><img alt="" digit""="" only="" src="./Images/Screens/TyreZoneFilter1.png"/></a> <a class="glightbox" href="./Images/Screens/TyreZoneFilter1.png"><img alt="" avec="" couleurs="" des="" le="" reste="" src="./Images/Screens/TyreZoneFilter1.png" supprimmées""="" zone=""/></a>
<figcaption>"Only digit"</figcaption> <figcaption>"Zone avec le reste des couleurs supprimmées"</figcaption>
</figure> </figure>
<p>Et la on a ce que l'on veut !</p> <p>Et la on a ce que l'on veut !</p>
<p>A partir de la c'est les filtres que l'on connait qui sont utilisés pour en faire une image plus facile à utiliser par Tesseract.</p> <p>A partir de la c'est les filtres que l'on connait qui sont utilisés pour en faire une image plus facile à utiliser par Tesseract.</p>
<p>1 : On effectue un <em>Resize</em> de facteur 4 (oui c'est beaucoup mais en même temps le chiffre est vraiment petit à la base) qui permet d'avoir une image d'une bien meilleure résolution.</p> <p>1 : On effectue un <em>Resize</em> de facteur 4 (oui c'est beaucoup mais en même temps le chiffre est vraiment petit à la base) qui permet d'avoir une image d'une bien meilleure résolution.</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/TyreZoneFilter2.png"><img 1""="" alt="" filter="" src="./Images/Screens/TyreZoneFilter2.png"/></a> <a class="glightbox" href="./Images/Screens/TyreZoneFilter2.png"><img 1""="" alt="" filtre="" src="./Images/Screens/TyreZoneFilter2.png"/></a>
<figcaption>"Filter 1"</figcaption> <figcaption>"Filtre 1"</figcaption>
</figure> </figure>
<p>2: On fait une <em>Dilatation</em> de facteur 1 pour retirer tout le flou de l'image pour aider Tesseract</p> <p>2: On fait une <em>Dilatation</em> de facteur 1 pour retirer tout le flou de l'image pour aider Tesseract</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Screens/TyreZoneAfter.png"><img alt="" result""="" src="./Images/Screens/TyreZoneAfter.png"/></a> <a class="glightbox" href="./Images/Screens/TyreZoneAfter.png"><img alt="" resultat""="" src="./Images/Screens/TyreZoneAfter.png"/></a>
<figcaption>"Result"</figcaption> <figcaption>"Resultat"</figcaption>
</figure> </figure>
<p>Et on a un chiffre qui est utilisable par Tesseract !</p> <p>Et on a un chiffre qui est utilisable par Tesseract !</p>
<p><em>Explication des methodes précises plus bas</em></p> <p><em>Explication des methodes précises plus bas</em></p>
@@ -1640,23 +1663,23 @@ C'est une étape très importante pour l'OCR car elle permet (si bien faite) d'i
<p>Cette methode sert à augmenter la résolution d'une image pour améliorer la précision de l'algorythme de Tesseract. En effet, avec trop peu de pixels, la matrice de convolution n'est pas toujours aussi efficace.</p> <p>Cette methode sert à augmenter la résolution d'une image pour améliorer la précision de l'algorythme de Tesseract. En effet, avec trop peu de pixels, la matrice de convolution n'est pas toujours aussi efficace.</p>
<p>Il ne faut pas confondre cette methode d'augmentation de la taille avec une simple interpolation. En effet une augmentation de taille interpolée ne vas pas vraiment changer la résolution, l'image sera toujours aussi pixelisée, seulement, les pixels seront composées de plus de pixels comme dans l'exemple ci dessous :</p> <p>Il ne faut pas confondre cette methode d'augmentation de la taille avec une simple interpolation. En effet une augmentation de taille interpolée ne vas pas vraiment changer la résolution, l'image sera toujours aussi pixelisée, seulement, les pixels seront composées de plus de pixels comme dans l'exemple ci dessous :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Photos/InterpolationExemple.png"><img alt="" exemple""="" interpolation="" src="./Images/Photos/InterpolationExemple.png"/></a> <a class="glightbox" href="./Images/Photos/InterpolationExemple.png"><img alt="" d'interpolation="" exemple="" linéaire""="" src="./Images/Photos/InterpolationExemple.png"/></a>
<figcaption>"Interpolation exemple"</figcaption> <figcaption>"Exemple d'interpolation linéaire"</figcaption>
</figure> </figure>
<p>Dans mon projet j'utilise de l'interpolation bicubique qui va créer de l'information pour tenter de combler le vide et produire une image réellement plus grande et avec plus de details mais en ajoutant du flou.</p> <p>Dans mon projet j'utilise de l'interpolation bicubique qui va créer de l'information pour tenter de combler le vide et produire une image réellement plus grande et avec plus de details mais en ajoutant du flou.</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Photos/BicubicExample.png"><img alt="" bicubic="" exemple""="" src="./Images/Photos/BicubicExample.png"/></a> <a class="glightbox" href="./Images/Photos/BicubicExample.png"><img alt="" d'interpolation""="" des="" différents="" exemple="" src="./Images/Photos/BicubicExample.png" types=""/></a>
<figcaption>"bicubic exemple"</figcaption> <figcaption>"Exemple des différents types d'interpolation"</figcaption>
</figure> </figure>
<p>Le but est d'aller chercher dans les pixels alentours les couleurs qui sont déja présente et de jouer avec des poids pour tenter de faire une prédiction de ce que ce pixel aurait été si l'image avait plus de detail.</p> <p>Le but est d'aller chercher dans les pixels alentours les couleurs qui sont déja présente et de jouer avec des poids pour tenter de faire une prédiction de ce que ce pixel aurait été si l'image avait plus de detail.</p>
<p>Voici un exemple assez parlant :</p> <p>Voici un exemple assez parlant :</p>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Photos/BicubicExample1.webp"><img alt="" bicubic="" demonstration""="" src="./Images/Photos/BicubicExample1.webp"/></a> <a class="glightbox" href="./Images/Photos/BicubicExample1.webp"><img (avant)""="" alt="" bicubique="" exemple="" interpolation="" src="./Images/Photos/BicubicExample1.webp"/></a>
<figcaption>"bicubic demonstration"</figcaption> <figcaption>"Exemple interpolation bicubique (avant)"</figcaption>
</figure> </figure>
<figure class="figure-image"> <figure class="figure-image">
<a class="glightbox" href="./Images/Photos/BicubicExample2.webp"><img alt="" bicubic="" demonstration""="" src="./Images/Photos/BicubicExample2.webp"/></a> <a class="glightbox" href="./Images/Photos/BicubicExample2.webp"><img (après)""="" alt="" bicubique="" exemple="" interpolation="" src="./Images/Photos/BicubicExample2.webp"/></a>
<figcaption>"bicubic demonstration"</figcaption> <figcaption>"Exemple interpolation bicubique (après)"</figcaption>
</figure> </figure>
<p>On pourrait croire que c'est inutile mais dans le contexte de Tesseract ajouter des détails pour tenter de simuler une meilleure résolution même en créant du flou est intéressant pour mieux remplir la matrice de convolution.</p> <p>On pourrait croire que c'est inutile mais dans le contexte de Tesseract ajouter des détails pour tenter de simuler une meilleure résolution même en créant du flou est intéressant pour mieux remplir la matrice de convolution.</p>
<p>Mais il est possible de réduire ce flou avec d'autres méthodes également.</p> <p>Mais il est possible de réduire ce flou avec d'autres méthodes également.</p>
@@ -1680,6 +1703,12 @@ C'est une étape très importante pour l'OCR car elle permet (si bien faite) d'i
<h2 id="resume-des-difficultes-techniques">Résumé des difficultés techniques</h2> <h2 id="resume-des-difficultes-techniques">Résumé des difficultés techniques</h2>
<hr/> <hr/>
<p>[A remplir au fur et à mesure dans la seconde moitié du travail de diplôme]</p> <p>[A remplir au fur et à mesure dans la seconde moitié du travail de diplôme]</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>
<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>
<h2 id="ameliorations-futures">Améliorations futures</h2> <h2 id="ameliorations-futures">Améliorations futures</h2>
<hr/> <hr/>
<p>[A remplir dans les dernières semaines du travail de diplôme]</p> <p>[A remplir dans les dernières semaines du travail de diplôme]</p>
+5410 -5188
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.
+198
View File
@@ -0,0 +1,198 @@
# ConfigurationTool.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 08/05/2023
/// File : ConfigurationTool.cs
/// Brief : Class that contains all the methods needed to create a config file for the OCR
/// Version : 0.1
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tesseract;
using System.IO;
namespace Test_Merge
{
public class ConfigurationTool
{
public Zone MainZone;
public const int NUMBER_OF_DRIVERS = 20;
public const int NUMBER_OF_ZONES = 9;
public const string CONFIGS_FOLDER_NAME = "./Presets/";
public ConfigurationTool(Bitmap fullImage, Rectangle mainZoneDimensions)
{
MainZone = new Zone(fullImage, mainZoneDimensions,"Main");
AutoCalibrate();
}
public void ResetMainZone()
{
MainZone.ResetZones();
}
public void ResetWindows()
{
MainZone.ResetWindows();
}
public void SaveToJson(List<string> drivers, string configName)
{
string JSON = "";
JSON += "{" + Environment.NewLine;
JSON += MainZone.ToJSON() + "," + Environment.NewLine;
JSON += "\"Drivers\":[" + Environment.NewLine;
for (int i = 0; i < drivers.Count; i++)
{
JSON += "\"" + drivers[i] + "\"";
if (i < drivers.Count - 1)
JSON += ",";
JSON += Environment.NewLine;
}
JSON += "]" + Environment.NewLine;
JSON += "}";
if (!Directory.Exists(CONFIGS_FOLDER_NAME))
Directory.CreateDirectory(CONFIGS_FOLDER_NAME);
string path = CONFIGS_FOLDER_NAME + configName;
if (File.Exists(path + ".json"))
{
//We need to create a new name
int count = 2;
while (File.Exists(path + "_" + count + ".json"))
{
count++;
}
path += "_" + count + ".json";
}
else
{
path += ".json";
}
File.WriteAllText(path, JSON);
}
public void AddWindows(List<Rectangle> rectangles)
{
foreach (Zone driverZone in MainZone.Zones)
{
Bitmap zoneImage = driverZone.ZoneImage;
for (int i = 1; i <= rectangles.Count; i++)
{
switch (i)
{
case 1:
//First zone should be the driver's Position
driverZone.AddWindow(new DriverPositionWindow(driverZone.ZoneImage, rectangles[i - 1], false));
break;
case 2:
//First zone should be the Gap to leader
driverZone.AddWindow(new DriverGapToLeaderWindow(driverZone.ZoneImage, rectangles[i - 1], false));
break;
case 3:
//First zone should be the driver's Lap Time
driverZone.AddWindow(new DriverLapTimeWindow(driverZone.ZoneImage, rectangles[i - 1], false));
break;
case 4:
//First zone should be the driver's DRS status
driverZone.AddWindow(new DriverDrsWindow(driverZone.ZoneImage, rectangles[i - 1], false));
break;
case 5:
//First zone should be the driver's Tyre's informations
driverZone.AddWindow(new DriverTyresWindow(driverZone.ZoneImage, rectangles[i - 1], false));
break;
case 6:
//First zone should be the driver's Name
driverZone.AddWindow(new DriverNameWindow(driverZone.ZoneImage, rectangles[i - 1], false));
break;
case 7:
//First zone should be the driver's First Sector
driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 1, false));
break;
case 8:
//First zone should be the driver's Second Sector
driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 2, false));
break;
case 9:
//First zone should be the driver's Position Sector
driverZone.AddWindow(new DriverSectorWindow(driverZone.ZoneImage, rectangles[i - 1], 3, false));
break;
}
}
}
}
public void AutoCalibrate()
{
List<Rectangle> detectedText = new List<Rectangle>();
List<Zone> zones = new List<Zone>();
TesseractEngine engine = new TesseractEngine(Window.TESS_DATA_FOLDER.FullName, "eng", EngineMode.Default);
Image image = MainZone.ZoneImage;
var tessImage = Pix.LoadFromMemory(Window.ImageToByte(image));
Page page = engine.Process(tessImage);
using (var iter = page.GetIterator())
{
iter.Begin();
do
{
Rect boundingBox;
if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox))
{
//var text = iter.GetText(PageIteratorLevel.Word).ToUpper();
//We remove all the rectangles that are definitely too big
if (boundingBox.Height < image.Height / NUMBER_OF_DRIVERS)
{
//Now we add a filter to only get the boxes in the right because they are much more reliable in size
if (boundingBox.X1 > image.Width / 2)
{
//Now we check if an other square box has been found roughly in the same y axis
bool match = false;
//The tolerance is roughly half the size that a window will be
int tolerance = (image.Height / NUMBER_OF_DRIVERS) / 2;
foreach (Rectangle rect in detectedText)
{
if (rect.Y > boundingBox.Y1 - tolerance && rect.Y < boundingBox.Y1 + tolerance)
{
//There already is a rectangle in this line
match = true;
}
}
//if nothing matched we can add it
if (!match)
detectedText.Add(new Rectangle(boundingBox.X1, boundingBox.Y1, boundingBox.Width, boundingBox.Height));
}
}
}
} while (iter.Next(PageIteratorLevel.Word));
}
//DEBUG
int i = 1;
foreach (Rectangle Rectangle in detectedText)
{
Rectangle windowRectangle;
Size windowSize = new Size(image.Width, image.Height / NUMBER_OF_DRIVERS);
Point windowLocation = new Point(0, (Rectangle.Y + Rectangle.Height / 2) - windowSize.Height / 2);
windowRectangle = new Rectangle(windowLocation, windowSize);
//We add the driver zones
Zone driverZone = new Zone(MainZone.ZoneImage, windowRectangle, "DriverZone");
MainZone.AddZone(driverZone);
driverZone.ZoneImage.Save("Driver" + i+".png");
i++;
}
}
}
}
```
+107
View File
@@ -0,0 +1,107 @@
# DriverData.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 08/05/2023
/// File : DriverData.cs
/// Brief : Class used to store Driver informations
/// Version : 0.1
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Test_Merge
{
public class DriverData
{
public bool DRS; //True = Drs is opened
public int GapToLeader; //In ms
public int LapTime; //In ms
public string Name; //Ex: LECLERC
public int Position; //Ex: 1
public int Sector1; //in ms
public int Sector2; //in ms
public int Sector3; //in ms
public Tyre CurrentTyre;//Ex Soft 11 laps
public DriverData(bool dRS, int gapToLeader, int lapTime, string name, int position, int sector1, int sector2, int sector3, Tyre tyre)
{
DRS = dRS;
GapToLeader = gapToLeader;
LapTime = lapTime;
Name = name;
Position = position;
Sector1 = sector1;
Sector2 = sector2;
Sector3 = sector3;
CurrentTyre = tyre;
}
public DriverData()
{
DRS = false;
GapToLeader = -1;
LapTime = -1;
Name = "Unknown";
Position = -1;
Sector1 = -1;
Sector2 = -1;
Sector3 = -1;
CurrentTyre = new Tyre(Tyre.Type.Undefined, -1);
}
/// <summary>
/// Method that displays all the data found in a string
/// </summary>
/// <returns>string containing all the driver datas</returns>
public override string ToString()
{
string result = "";
//Position
result += "Position : " + Position + Environment.NewLine;
//Gap
result += "GapToLeader : " + Reader.ConvertMsToTime(GapToLeader) + Environment.NewLine;
//LapTime
result += "LapTime : " + Reader.ConvertMsToTime(LapTime) + Environment.NewLine;
//DRS
result += "DRS : " + DRS + Environment.NewLine;
//Tyres
result += "Uses " + CurrentTyre.Coumpound + " tyre " + CurrentTyre.NumberOfLaps + " laps old" + Environment.NewLine;
//Name
result += "DriverName : " + Name + Environment.NewLine;
//Sector 1
result += "Sector1 : " + Reader.ConvertMsToTime(Sector1) + Environment.NewLine;
//Sector 1
result += "Sector2 : " + Reader.ConvertMsToTime(Sector2) + Environment.NewLine;
//Sector 1
result += "Sector3 : " + Reader.ConvertMsToTime(Sector3) + Environment.NewLine;
return result;
}
}
//Structure to store tyres infos
public struct Tyre
{
//If new tyres were to be added you will have to need to change this enum
public enum Type
{
Soft,
Medium,
Hard,
Inter,
Wet,
Undefined
}
public Type Coumpound;
public int NumberOfLaps;
public Tyre(Type type, int laps)
{
Coumpound = type;
NumberOfLaps = laps;
}
}
}
```
+103
View File
@@ -0,0 +1,103 @@
# DriverDrsWindow.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 08/05/2023
/// File : DriverDrsWindow.cs
/// Brief : Window containing DRS related method and infos
/// Version : 0.1
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tesseract;
namespace Test_Merge
{
internal class DriverDrsWindow:Window
{
private static int EmptyDrsGreenValue = -1;
private static Random rnd = new Random();
public DriverDrsWindow(Bitmap image, Rectangle bounds,bool generateEngine = true) : base(image, bounds,generateEngine)
{
Name = "DRS";
}
public override async Task<object> DecodePng()
{
bool result = false;
int greenValue = GetGreenPixels();
if (EmptyDrsGreenValue == -1)
EmptyDrsGreenValue = greenValue;
if (greenValue > EmptyDrsGreenValue + EmptyDrsGreenValue / 100 * 30)
result = true;
return result;
}
private unsafe int GetGreenPixels()
{
int tot = 0;
Bitmap bmp = WindowImage;
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
unsafe
{
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < bmp.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < bmp.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
byte blue = pixel[0];
byte green = pixel[1];
byte red = pixel[2];
if (green > blue * 1.5 && green > red * 1.5)
{
tot++;
}
}
}
}
bmp.UnlockBits(bmpData);
return tot;
}
public Rectangle GetBox()
{
var tessImage = Pix.LoadFromMemory(ImageToByte(WindowImage));
Engine.SetVariable("tessedit_char_whitelist", "");
Page page = Engine.Process(tessImage);
using (var iter = page.GetIterator())
{
iter.Begin();
do
{
Rect boundingBox;
// Get the bounding box for the current element
if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox))
{
page.Dispose();
return new Rectangle(boundingBox.X1, boundingBox.X2, boundingBox.Width, boundingBox.Height);
}
} while (iter.Next(PageIteratorLevel.Word));
page.Dispose();
return new Rectangle(0, 0, 0, 0);
}
}
}
}
```
@@ -0,0 +1,37 @@
# DriverGapToLeaderWindow.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 08/05/2023
/// File : DriverGapToLeaderWindow.cs
/// Brief : Window containing infos about the gap to the leader of a driver
/// Version : 0.1
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Test_Merge
{
internal class DriverGapToLeaderWindow:Window
{
public DriverGapToLeaderWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine)
{
Name = "GapToLeader";
}
/// <summary>
/// Decodes the gap to leader using Tesseract OCR
/// </summary>
/// <returns></returns>
public override async Task<object> DecodePng()
{
int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Gap, Engine);
return result;
}
}
}
```
+37
View File
@@ -0,0 +1,37 @@
# DriverLapTimeWindow.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 08/05/2023
/// File : DriverLapTimeWindow
/// Brief : Window containing infos about the lap time of a driver
/// Version : 0.1
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
namespace Test_Merge
{
internal class DriverLapTimeWindow:Window
{
public DriverLapTimeWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine)
{
Name = "LapTime";
}
/// <summary>
/// Decodes the lap time contained in the image using OCR Tesseract
/// </summary>
/// <returns>The laptime in int (ms)</returns>
public override async Task<object> DecodePng()
{
int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.LapTime, Engine);
return result;
}
}
}
```
+63
View File
@@ -0,0 +1,63 @@
# DriverNameWindow.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 08/05/2023
/// File : DriverNameWindow
/// Brief : Window containing infos about the name of the driver
/// Version : 0.1
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
namespace Test_Merge
{
public class DriverNameWindow : Window
{
public static Random rnd = new Random();
public DriverNameWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine)
{
Name = "Name";
}
/// <summary>
/// Decodes using OCR wich driver name is in the image
/// </summary>
/// <param name="DriverList"></param>
/// <returns>The driver name in string</returns>
public override async Task<object> DecodePng(List<string> DriverList)
{
string result = "";
result = await GetStringFromPng(WindowImage, Engine);
if (!IsADriver(DriverList, result))
{
//I put everything in uppercase to try to lower the chances of bad answers
result = FindClosestMatch(DriverList.ConvertAll(d => d.ToUpper()), result.ToUpper());
}
return result;
}
/// <summary>
/// Verifies that the name found in the OCR is a valid name
/// </summary>
/// <param name="driverList"></param>
/// <param name="potentialDriver"></param>
/// <returns>If ye or no the driver exists</returns>
private static bool IsADriver(List<string> driverList, string potentialDriver)
{
bool result = false;
//I cant use drivers.Contains because it has missmatched cases and all
foreach (string name in driverList)
{
if (name.ToUpper() == potentialDriver.ToUpper())
result = true;
}
return result;
}
}
}
```
+47
View File
@@ -0,0 +1,47 @@
# DriverPositionWindow.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 08/05/2023
/// File : DriverPosition.cs
/// Brief : Window containing infos about the position of a driver.
/// Version : 0.1
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
namespace Test_Merge
{
public class DriverPositionWindow:Window
{
public DriverPositionWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine)
{
Name = "Position";
}
/// <summary>
/// Decodes the position number using Tesseract OCR
/// </summary>
/// <returns>The position of the pilot in int</returns>
public override async Task<object> DecodePng()
{
string ocrResult = await GetStringFromPng(WindowImage, Engine, "0123456789");
int position;
try
{
position = Convert.ToInt32(ocrResult);
}
catch
{
position = -1;
}
return position;
}
}
}
```
+37
View File
@@ -0,0 +1,37 @@
# DriverSectorWindow.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 08/05/2023
/// File : DriverSectorWindow.cs
/// Brief : Window containing infos about a driver sector time. Can be the first second or third, does not matter.
/// Version : 0.1
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
namespace Test_Merge
{
internal class DriverSectorWindow:Window
{
public DriverSectorWindow(Bitmap image, Rectangle bounds, int sectorId, bool generateEngine = true) : base(image, bounds,generateEngine)
{
Name = "Sector"+sectorId;
}
/// <summary>
/// Decodes the sector
/// </summary>
/// <returns>the sector time in int (ms)</returns>
public override async Task<object> DecodePng()
{
int ocrResult = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector, Engine);
return ocrResult;
}
}
}
```
+146
View File
@@ -0,0 +1,146 @@
# DriverTyresWindow.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 08/05/2023
/// File : DriverTyresWindow.cs
/// Brief : Window containing infos about a driver's tyre
/// Version : 0.1
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
namespace Test_Merge
{
public class DriverTyresWindow:Window
{
private static Random rnd = new Random();
int seed = rnd.Next(0, 10000);
//Those are the colors I found but you can change them if they change in the future like in 2019
public static Color SOFT_TYRE_COLOR = Color.FromArgb(0xff, 0x00, 0x00);
public static Color MEDIUM_TYRE_COLOR = Color.FromArgb(0xf5, 0xbf, 0x00);
public static Color HARD_TYRE_COLOR = Color.FromArgb(0xa4, 0xa5, 0xa8);
public static Color INTER_TYRE_COLOR = Color.FromArgb(0x00, 0xa4, 0x2e);
public static Color WET_TYRE_COLOR = Color.FromArgb(0x27, 0x60, 0xa6);
public static Color EMPTY_COLOR = Color.FromArgb(0x20, 0x20, 0x20);
public DriverTyresWindow(Bitmap image, Rectangle bounds, bool generateEngine = true) : base(image, bounds,generateEngine)
{
Name = "Tyres";
}
/// <summary>
/// This will decode the content of the image
/// </summary>
/// <returns>And object containing what was on the image</returns>
public override async Task<object> DecodePng()
{
return await GetTyreInfos();
}
/// <summary>
/// Method that will decode whats on the image and return the tyre infos it could manage to recover
/// </summary>
/// <returns>A tyre object containing tyre infos</returns>
private async Task<Tyre> GetTyreInfos()
{
Bitmap tyreZone = GetSmallBitmapFromBigOne(WindowImage, FindTyreZone());
Tyre.Type type = Tyre.Type.Undefined;
type = GetTyreTypeFromColor(OcrImage.GetAvgColorFromBitmap(tyreZone));
int laps = -1;
string number = await GetStringFromPng(tyreZone, Engine, "0123456789", OcrImage.WindowType.Tyre);
try
{
laps = Convert.ToInt32(number);
}
catch
{
//We could not convert the number so its a letter so its 0 laps old
laps = 0;
}
//tyreZone.Save(Reader.DEBUG_DUMP_FOLDER + "Tyre" + type + "Laps" + laps + '#' + rnd.Next(0, 1000) + ".png");
return new Tyre(type, laps);
}
/// <summary>
/// Finds where the important part of the image is
/// </summary>
/// <returns>A rectangle containing position and dimensions of the important part of the image</returns>
private Rectangle FindTyreZone()
{
Bitmap bmp = WindowImage;
int currentPosition = bmp.Width;
int height = bmp.Height / 2;
Color limitColor = Color.FromArgb(0x50, 0x50, 0x50);
Color currentColor = Color.FromArgb(0, 0, 0);
Size newWindowSize = new Size(bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 25f), bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 35f));
while (currentColor.R <= limitColor.R && currentColor.G <= limitColor.G && currentColor.B <= limitColor.B && currentPosition > 0)
{
currentPosition--;
currentColor = bmp.GetPixel(currentPosition, height);
}
//Its here to let the new window include a little bit of the right
int CorrectedX = currentPosition - (newWindowSize.Width) + Convert.ToInt32((float)newWindowSize.Width / 100f * 10f);
int CorrectedY = Convert.ToInt32((float)newWindowSize.Height / 100f * 35f);
if (CorrectedX <= 0)
return new Rectangle(0, 0, newWindowSize.Width, newWindowSize.Height);
return new Rectangle(CorrectedX, CorrectedY, newWindowSize.Width, newWindowSize.Height);
}
//This method has been created with the help of chatGPT
/// <summary>
/// Methods that compares a list of colors to see wich is the closest from the input color and decide wich tyre type it is
/// </summary>
/// <param name="inputColor">The color that you found</param>
/// <returns>The tyre type</returns>
public Tyre.Type GetTyreTypeFromColor(Color inputColor)
{
Tyre.Type type = Tyre.Type.Undefined;
List<Color> colors = new List<Color>();
//dont forget that if for some reason someday F1 adds a new Tyre type you will need to add it in the constants but also here in the list
//You will also need to add it below in the Tyre object's enum and add an if in the end of this method
colors.Add(SOFT_TYRE_COLOR);
colors.Add(MEDIUM_TYRE_COLOR);
colors.Add(HARD_TYRE_COLOR);
colors.Add(INTER_TYRE_COLOR);
colors.Add(WET_TYRE_COLOR);
colors.Add(EMPTY_COLOR);
Color closestColor = colors[0];
int closestDistance = int.MaxValue;
foreach (Color color in colors)
{
int distance = Math.Abs(color.R - inputColor.R) + Math.Abs(color.G - inputColor.G) + Math.Abs(color.B - inputColor.B);
if (distance < closestDistance)
{
closestColor = color;
closestDistance = distance;
}
}
//We cant use a switch as the colors cant be constants ...
if (closestColor == SOFT_TYRE_COLOR)
type = Tyre.Type.Soft;
if (closestColor == MEDIUM_TYRE_COLOR)
type = Tyre.Type.Medium;
if (closestColor == HARD_TYRE_COLOR)
type = Tyre.Type.Hard;
if (closestColor == INTER_TYRE_COLOR)
type = Tyre.Type.Inter;
if (closestColor == WET_TYRE_COLOR)
type = Tyre.Type.Wet;
if (closestColor == EMPTY_COLOR)
return Tyre.Type.Undefined;
return type;
}
}
}
```
+293
View File
@@ -0,0 +1,293 @@
# F1TVEmulator.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 08/05/2023
/// File : F1TVEmulator.cs
/// Brief : Class that contains methods to emulate a browser and navigate the F1TV website
/// Version : 0.1
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Interactions;
using OpenQA.Selenium.Support.UI;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Test_Merge
{
internal class F1TVEmulator
{
public const string COOKIE_HOST = ".formula1.com";
public const string PYTHON_COOKIE_RETRIEVAL_FILENAME = "recoverCookiesCSV.py";
public const string GECKODRIVER_FILENAME = @"geckodriver-v0.27.0-win64\geckodriver.exe";
//BE CAREFULL IF YOU CHANGE IT HERE YOU NEED TO CHANGE IT IN THE PYTHON SCRIPT TOO
public const string COOKIES_CSV_FILENAME = "cookies.csv";
private FirefoxDriver Driver;
private bool _ready;
private string _grandPrixUrl;
public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; }
public bool Ready { get => _ready; set => _ready = value; }
public F1TVEmulator(string grandPrixUrl)
{
GrandPrixUrl = grandPrixUrl;
Ready = false;
}
private void StartCookieRecovering()
{
string scriptPath = PYTHON_COOKIE_RETRIEVAL_FILENAME;
Process process = new Process();
process.StartInfo.FileName = "python.exe";
process.StartInfo.Arguments = scriptPath;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.Start();
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
}
public string GetCookie(string host, string name)
{
StartCookieRecovering();
string value = "";
List<Cookie> cookies = new List<Cookie>();
using (var reader = new StreamReader(COOKIES_CSV_FILENAME))
{
// Read the header row and validate column order
string header = reader.ReadLine();
string[] expectedColumns = { "host_key", "name", "value", "path", "expires_utc", "is_secure", "is_httponly" };
string[] actualColumns = header.Split(',');
for (int i = 0; i < expectedColumns.Length; i++)
{
if (expectedColumns[i] != actualColumns[i])
{
throw new InvalidOperationException($"Expected column '{expectedColumns[i]}' at index {i} but found '{actualColumns[i]}'");
}
}
// Read each data row and parse values into a Cookie object
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
string[] fields = line.Split(',');
string hostname = fields[0];
string cookieName = fields[1];
if (hostname == host && cookieName == name)
{
value = fields[2];
}
}
}
return value;
}
public async Task<int> Start()
{
Ready = false;
string loginCookieName = "login";
string loginSessionCookieName = "login-session";
string loginCookieValue = GetCookie(COOKIE_HOST, loginCookieName);
string loginSessionValue = GetCookie(COOKIE_HOST, loginSessionCookieName);
int windowWidth = 1920;
int windowHeight = 768;
var service = FirefoxDriverService.CreateDefaultService(GECKODRIVER_FILENAME);
service.Host = "127.0.0.1";
service.Port = 5555;
FirefoxProfile profile = new FirefoxProfile();
FirefoxOptions options = new FirefoxOptions();
//profile.SetPreference("full-screen-api.ignore-widgets", true);
//profile.SetPreference("media.hardware-video-decoding.enabled", true);
//profile.SetPreference("full-screen-api.enabled", true);
options.Profile = profile;
profile.SetPreference("layout.css.devPixelsPerPx", "1.0");
options.AcceptInsecureCertificates = true;
options.AddArgument("--headless");
//options.AddArgument("--start-maximized");
//options.AddArgument("--window-size=1920x1080");
//options.AddArgument("--width=" + windowWidth);
//options.AddArgument("--height=" + windowHeight);
//options.AddArgument("-window-size=1920x1080");
//options.AddArgument("--width=1920");
//options.AddArgument("--height=1080");
//profile
try
{
Driver = new FirefoxDriver(service, options);
}
catch
{
Ready = false;
return 101;
}
Actions actions = new Actions(Driver);
var loginCookie = new Cookie(loginCookieName, loginCookieValue, COOKIE_HOST, "/", DateTime.Now.AddDays(5));
var loginSessionCookie = new Cookie(loginSessionCookieName, loginSessionValue, COOKIE_HOST, "/", DateTime.Now.AddDays(5));
Driver.Navigate().GoToUrl("https://f1tv.formula1.com/");
Driver.Manage().Cookies.AddCookie(loginCookie);
Driver.Manage().Cookies.AddCookie(loginSessionCookie);
try
{
Driver.Navigate().GoToUrl(GrandPrixUrl);
}
catch
{
//The url is not a valid url
Driver.Dispose();
return 103;
}
//Waits for the page to fully load
Driver.Manage().Timeouts().PageLoad = TimeSpan.FromSeconds(30);
//Removes the cookie prompt
try
{
IWebElement conscentButton = Driver.FindElement(By.Id("truste-consent-button"));
conscentButton.Click();
}
catch
{
//Could not locate the cookie button
Screenshot("ERROR104");
Driver.Dispose();
return 104;
}
//Again waits for the page to fully load (when you accept cookies it takes a little time for the page to load)
//Cannot use The timeout because the feed loading is not really loading so there is not event or anything
Thread.Sleep(5000);
//Switches to the Data channel
try
{
IWebElement dataChannelButton = Driver.FindElement(By.ClassName("data-button"));
dataChannelButton.Click();
}
catch
{
//If the data button does not exists its because the user is not connected
Screenshot("ERROR102");
Driver.Dispose();
return 102;
}
//Open settings
// Press the space key, this should make the setting button visible
// It does not matter if the feed is paused because when changing channel it autoplays
actions.SendKeys(OpenQA.Selenium.Keys.Space).Perform();
//Clicks on the settings Icon
int tries = 0;
bool success = false;
while (tries < 100 && !success)
{
Thread.Sleep(100);
try
{
IWebElement settingsButton = Driver.FindElement(By.ClassName("bmpui-ui-settingstogglebutton"));
settingsButton.Click();
IWebElement selectElement = Driver.FindElement(By.ClassName("bmpui-ui-videoqualityselectbox"));
SelectElement select = new SelectElement(selectElement);
IWebElement selectOption = selectElement.FindElement(By.CssSelector("option[value^='1080_']"));
selectOption.Click();
success = true;
}
catch
{
//Sometimes it can crash because it could not get the options to show up in time. When it happens just retry
success = false;
tries++;
}
}
if (!success)
{
Screenshot("ERROR105");
Driver.Dispose();
return 105;
}
Screenshot("BEFOREFULLSCREEN");
//Makes the feed fullscreen
//Driver.Manage().Window.Size = new System.Drawing.Size(windowWidth, windowHeight);
Driver.Manage().Window.Maximize();
WebDriverWait wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10));
try
{
IWebElement fullScreenButton = Driver.FindElement(By.ClassName("bmpui-ui-fullscreentogglebutton"));
fullScreenButton.Click();
}
catch
{
Screenshot("ERROR106");
Driver.Dispose();
return 106;
}
Screenshot("AFTERFULLSCREEN");
//STARTUP FINISHED READY TO SCREENSHOT
Ready = true;
return 0;
}
public Bitmap Screenshot(string name = "TEST")
{
Bitmap result = new Bitmap(4242, 6969);
try
{
//Screenshot scrsht = ((ITakesScreenshot)Driver).GetScreenshot();
//profileriver.SetPreference("layout.css.devPixelsPerPx", "1.0");
//Screenshot scrsht = Driver.GetFullPageScreenshot();
Screenshot scrsht = Driver.GetScreenshot();
byte[] screenshotBytes = Convert.FromBase64String(scrsht.AsBase64EncodedString);
MemoryStream stream = new MemoryStream(screenshotBytes);
result = new Bitmap(stream);
//result.Save(name + ".png");
scrsht.SaveAsFile(name + ".png");
}
catch
{
//Nothing for now
}
return result;
}
public void Stop()
{
Ready = false;
Driver.Dispose();
}
public void ResetDriver()
{
Ready = false;
Driver.Dispose();
Driver = null;
}
}
}
```
+32
View File
@@ -0,0 +1,32 @@
# Form1.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;
namespace Test_Merge
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void btnSettings_Click(object sender, EventArgs e)
{
Settings settingsForm = new Settings();
settingsForm.ShowDialog();
MessageBox.Show(settingsForm.GrandPrixUrl + Environment.NewLine + settingsForm.GrandPrixName + Environment.NewLine + settingsForm.GrandPrixYear);
}
}
}
```
+544
View File
@@ -0,0 +1,544 @@
# OcrImage.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 08/05/2023
/// File : OcrImage.cs
/// Brief : Class containing all the methods used to enhance images for OCR
/// Version : 0.1
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
namespace Test_Merge
{
public class OcrImage
{
//this is a hardcoded value based on the colors of the F1TV data channel background you can change it if sometime in the future the color changes
//Any color that has any of its R,G or B channel higher than the treshold will be considered as being usefull information
public static Color F1TV_BACKGROUND_TRESHOLD = Color.FromArgb(0x50, 0x50, 0x50);
Bitmap InputBitmap;
public enum WindowType
{
LapTime,
Text,
Sector,
Gap,
Tyre,
}
/// <summary>
/// Create a new Ocr image to help enhance the given bitmap for OCR
/// </summary>
/// <param name="inputBitmap">The image you want to enhance</param>
public OcrImage(Bitmap inputBitmap)
{
InputBitmap = inputBitmap;
}
/// <summary>
/// Enhances the image depending on wich type of window the image comes from
/// </summary>
/// <param name="type">The type of the window. Depending on it different enhancing features will be applied</param>
/// <returns>The enhanced Bitmap</returns>
public Bitmap Enhance(WindowType type = WindowType.Text)
{
Bitmap outputBitmap = (Bitmap)InputBitmap.Clone();
switch (type)
{
case WindowType.LapTime:
outputBitmap = Tresholding(outputBitmap, 185);
outputBitmap = Resize(outputBitmap, 2);
outputBitmap = Dilatation(outputBitmap, 1);
outputBitmap = Erode(outputBitmap, 1);
break;
case WindowType.Text:
outputBitmap = InvertColors(outputBitmap);
outputBitmap = Tresholding(outputBitmap, 165);
outputBitmap = Resize(outputBitmap, 2);
outputBitmap = Dilatation(outputBitmap, 1);
break;
case WindowType.Tyre:
outputBitmap = RemoveUseless(outputBitmap);
outputBitmap = Resize(outputBitmap, 4);
outputBitmap = Dilatation(outputBitmap, 1);
break;
default:
outputBitmap = Tresholding(outputBitmap, 165);
outputBitmap = Resize(outputBitmap, 4);
outputBitmap = Erode(outputBitmap, 1);
break;
}
return outputBitmap;
}
/// <summary>
/// Method that convert a colored RGB bitmap into a GrayScale image
/// </summary>
/// <param name="inputBitmap">The Bitmap you want to convert</param>
/// <returns>The bitmap in grayscale</returns>
public static Bitmap Grayscale(Bitmap inputBitmap)
{
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
unsafe
{
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < inputBitmap.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < inputBitmap.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
byte blue = pixel[0];
byte green = pixel[1];
byte red = pixel[2];
//Those a specific values to correct the weights so its more pleasing to the human eye
int gray = (int)(red * 0.3 + green * 0.59 + blue * 0.11);
pixel[0] = pixel[1] = pixel[2] = (byte)gray;
}
}
}
inputBitmap.UnlockBits(bmpData);
return inputBitmap;
}
/// <summary>
/// Method that binaries the input image up to a certain treshold given
/// </summary>
/// <param name="inputBitmap">the bitmap you want to convert to binary colors</param>
/// <param name="threshold">The floor at wich the color is considered as white or black</param>
/// <returns>The binarised bitmap</returns>
public static Bitmap Tresholding(Bitmap inputBitmap, int threshold)
{
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
unsafe
{
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
int bmpHeight = inputBitmap.Height;
int bmpWidth = inputBitmap.Width;
Parallel.For(0, bmpHeight, y =>
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < bmpWidth; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
byte blue = pixel[0];
byte green = pixel[1];
byte red = pixel[2];
//Those a specific values to correct the weights so its more pleasing to the human eye
int gray = (int)(red * 0.3 + green * 0.59 + blue * 0.11);
int value = gray < threshold ? 0 : 255;
pixel[0] = pixel[1] = pixel[2] = (byte)value;
}
});
}
inputBitmap.UnlockBits(bmpData);
return inputBitmap;
}
/// <summary>
/// Method that removes the pixels that are flagged as background
/// </summary>
/// <param name="inputBitmap">The bitmap you want to remove the background from</param>
/// <returns>The Bitmap without the background</returns>
public static Bitmap RemoveBG(Bitmap inputBitmap)
{
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
unsafe
{
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < inputBitmap.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < inputBitmap.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
int B = pixel[0];
int G = pixel[1];
int R = pixel[2];
if (R <= F1TV_BACKGROUND_TRESHOLD.R && G <= F1TV_BACKGROUND_TRESHOLD.G && B <= F1TV_BACKGROUND_TRESHOLD.B)
pixel[0] = pixel[1] = pixel[2] = 0;
}
}
}
inputBitmap.UnlockBits(bmpData);
return inputBitmap;
}
/// <summary>
/// Method that removes all the useless things from the image and returns hopefully only the numbers
/// </summary>
/// <param name="inputBitmap">The bitmap you want to remove useless things from (Expects a cropped part of the TyreWindow)</param>
/// <returns>The bitmap with (hopefully) only the digits</returns>
public unsafe static Bitmap RemoveUseless(Bitmap inputBitmap)
{
//Note you can use something else than a cropped tyre window but I would recommend checking the code first to see if it fits your intended use
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < inputBitmap.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
List<int> pixelsToRemove = new List<int>();
bool fromBorder = true;
for (int x = 0; x < inputBitmap.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
int B = pixel[0];
int G = pixel[1];
int R = pixel[2];
if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R)
{
pixelsToRemove.Add(x);
}
else
{
if (fromBorder)
{
fromBorder = false;
pixelsToRemove.Add(x);
}
}
}
fromBorder = true;
for (int x = inputBitmap.Width - 1; x > 0; x--)
{
byte* pixel = currentLine + (x * bytesPerPixel);
int B = pixel[0];
int G = pixel[1];
int R = pixel[2];
if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R)
{
pixelsToRemove.Add(x);
}
else
{
if (fromBorder)
{
fromBorder = false;
pixelsToRemove.Add(x);
}
}
}
foreach (int pxPos in pixelsToRemove)
{
byte* pixel = currentLine + (pxPos * bytesPerPixel);
pixel[0] = 0xFF;
pixel[1] = 0xFF;
pixel[2] = 0xFF;
}
}
//Removing the color parts
for (int y = 0; y < inputBitmap.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < inputBitmap.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
int B = pixel[0];
int G = pixel[1];
int R = pixel[2];
if (R >= F1TV_BACKGROUND_TRESHOLD.R + 15 || G >= F1TV_BACKGROUND_TRESHOLD.G + 15 || B >= F1TV_BACKGROUND_TRESHOLD.B + 15)
{
pixel[0] = 0xFF;
pixel[1] = 0xFF;
pixel[2] = 0xFF;
}
}
}
inputBitmap.UnlockBits(bmpData);
return inputBitmap;
}
/// <summary>
/// Recovers the average colors from the Image. NOTE : It wont take in account colors that are lower than the background
/// </summary>
/// <param name="inputBitmap">The bitmap you want to get the average color from</param>
/// <returns>The average color of the bitmap</returns>
public static Color GetAvgColorFromBitmap(Bitmap inputBitmap)
{
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
int totR = 0;
int totG = 0;
int totB = 0;
int totPixels = 1;
unsafe
{
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
int bmpHeight = inputBitmap.Height;
int bmpWidth = inputBitmap.Width;
Parallel.For(0, bmpHeight, y =>
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < bmpWidth; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
int B = pixel[0];
int G = pixel[1];
int R = pixel[2];
if (R >= F1TV_BACKGROUND_TRESHOLD.R || G >= F1TV_BACKGROUND_TRESHOLD.G || B >= F1TV_BACKGROUND_TRESHOLD.B)
{
totPixels++;
totB += pixel[0];
totG += pixel[1];
totR += pixel[2];
}
}
});
}
inputBitmap.UnlockBits(bmpData);
return Color.FromArgb(255, Convert.ToInt32((float)totR / (float)totPixels), Convert.ToInt32((float)totG / (float)totPixels), Convert.ToInt32((float)totB / (float)totPixels));
}
/// <summary>
/// This method simply inverts all the colors in a Bitmap
/// </summary>
/// <param name="inputBitmap">the bitmap you want to invert the colors from</param>
/// <returns>The bitmap with inverted colors</returns>
public static Bitmap InvertColors(Bitmap inputBitmap)
{
Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
unsafe
{
byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < inputBitmap.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < inputBitmap.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
pixel[0] = (byte)(255 - pixel[0]);
pixel[1] = (byte)(255 - pixel[1]);
pixel[2] = (byte)(255 - pixel[2]);
}
}
}
inputBitmap.UnlockBits(bmpData);
return inputBitmap;
}
/// <summary>
/// Methods that applies Bicubic interpolation to increase the size and resolution of an image
/// </summary>
/// <param name="inputBitmap">The bitmap you want to resize</param>
/// <param name="resizeFactor">The factor of resizing you want to use. I recommend using even numbers</param>
/// <returns>The bitmap witht the new size</returns>
public static Bitmap Resize(Bitmap inputBitmap, int resizeFactor)
{
var resultBitmap = new Bitmap(inputBitmap.Width * resizeFactor, inputBitmap.Height * resizeFactor);
using (var graphics = Graphics.FromImage(resultBitmap))
{
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.DrawImage(inputBitmap, new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height));
}
return resultBitmap;
}
/// <summary>
/// method that Highlights the countours of a Bitmap
/// </summary>
/// <param name="inputBitmap">The bitmap you want to highlight the countours of</param>
/// <returns>The bitmap with countours highlighted</returns>
public static Bitmap HighlightContours(Bitmap inputBitmap)
{
Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height);
Bitmap grayscale = Grayscale(inputBitmap);
Bitmap thresholded = Tresholding(grayscale, 128);
Bitmap dilated = Dilatation(thresholded, 3);
Bitmap eroded = Erode(dilated, 3);
for (int y = 0; y < inputBitmap.Height; y++)
{
for (int x = 0; x < inputBitmap.Width; x++)
{
Color pixel = inputBitmap.GetPixel(x, y);
Color dilatedPixel = dilated.GetPixel(x, y);
Color erodedPixel = eroded.GetPixel(x, y);
int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11);
int threshold = dilatedPixel.R;
if (gray > threshold)
{
outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255));
}
else if (gray <= threshold && erodedPixel.R == 0)
{
outputBitmap.SetPixel(x, y, Color.FromArgb(255, 0, 0));
}
else
{
outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0));
}
}
}
return outputBitmap;
}
/// <summary>
/// Method that that erodes the morphology of a bitmap
/// </summary>
/// <param name="inputBitmap">The bitmap you want to erode</param>
/// <param name="kernelSize">The amount of Erosion you want (be carefull its expensive on ressources)</param>
/// <returns>The Bitmap with the eroded contents</returns>
public static Bitmap Erode(Bitmap inputBitmap, int kernelSize)
{
Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height);
int[,] kernel = new int[kernelSize, kernelSize];
for (int i = 0; i < kernelSize; i++)
{
for (int j = 0; j < kernelSize; j++)
{
kernel[i, j] = 1;
}
}
for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++)
{
for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++)
{
bool flag = true;
for (int i = -kernelSize / 2; i <= kernelSize / 2; i++)
{
for (int j = -kernelSize / 2; j <= kernelSize / 2; j++)
{
Color pixel = inputBitmap.GetPixel(x + i, y + j);
int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11);
if (gray >= 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1)
{
flag = false;
break;
}
}
if (!flag)
{
break;
}
}
if (flag)
{
outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255));
}
else
{
outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0));
}
}
}
return outputBitmap;
}
/// <summary>
/// Method that that use dilatation of the morphology of a bitmap
/// </summary>
/// <param name="inputBitmap">The bitmap you want to use dilatation on</param>
/// <param name="kernelSize">The amount of dilatation you want (be carefull its expensive on ressources)</param>
/// <returns>The Bitmap after Dilatation</returns>
public static Bitmap Dilatation(Bitmap inputBitmap, int kernelSize)
{
Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height);
int[,] kernel = new int[kernelSize, kernelSize];
for (int i = 0; i < kernelSize; i++)
{
for (int j = 0; j < kernelSize; j++)
{
kernel[i, j] = 1;
}
}
for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++)
{
for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++)
{
bool flag = false;
for (int i = -kernelSize / 2; i <= kernelSize / 2; i++)
{
for (int j = -kernelSize / 2; j <= kernelSize / 2; j++)
{
Color pixel = inputBitmap.GetPixel(x + i, y + j);
int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11);
if (gray < 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1)
{
flag = true;
break;
}
}
if (flag)
{
break;
}
}
if (flag)
{
outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0));
}
else
{
outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255));
}
}
}
return outputBitmap;
}
}
}
```
+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 Test_Merge
{
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}
```
+235
View File
@@ -0,0 +1,235 @@
# Reader.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 08/05/2023
/// File : Reader.cs
/// Brief : Class used to Read the config file for the OCR
/// Version : 0.1
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
using System.Windows.Forms;
using System.IO;
using System.Text.Json;
namespace Test_Merge
{
public class Reader
{
const int NUMBER_OF_DRIVERS = 20;
public List<string> Drivers;
public List<Zone> MainZones;
public Reader(string configFile, Bitmap image,bool loadOCR = true)
{
MainZones = Load(image,configFile,ref Drivers,loadOCR);
}
/// <summary>
/// Method that reads the JSON config file and create all the Zones and Windows
/// </summary>
/// <param name="imageNumber">The image #id on wich you want to create the zones on</param>
public static List<Zone> Load(Bitmap image,string configFilePath,ref List<string> driverListToFill,bool LoadOCR)
{
List<Zone> mainZones = new List<Zone>();
Bitmap fullImage = image;
List<string> drivers;
Zone mainZone;
try
{
using (var streamReader = new StreamReader(configFilePath))
{
var jsonText = streamReader.ReadToEnd();
var jsonDocument = JsonDocument.Parse(jsonText);
var driversNames = jsonDocument.RootElement.GetProperty("Drivers");
driverListToFill = new List<string>();
foreach (var nameElement in driversNames.EnumerateArray())
{
driverListToFill.Add(nameElement.GetString());
}
var mainProperty = jsonDocument.RootElement.GetProperty("Main");
Point MainPosition = new Point(mainProperty.GetProperty("x").GetInt32(), mainProperty.GetProperty("y").GetInt32());
Size MainSize = new Size(mainProperty.GetProperty("width").GetInt32(), mainProperty.GetProperty("height").GetInt32());
Rectangle MainRectangle = new Rectangle(MainPosition, MainSize);
mainZone = new Zone(image, MainRectangle,"Main");
var zones = mainProperty.GetProperty("Zones");
var driverZone = zones[0].GetProperty("DriverZone");
Point FirstZonePosition = new Point(driverZone.GetProperty("x").GetInt32(), driverZone.GetProperty("y").GetInt32());
Size FirstZoneSize = new Size(driverZone.GetProperty("width").GetInt32(), driverZone.GetProperty("height").GetInt32());
var windows = driverZone.GetProperty("Windows");
var driverPosition = windows[0].GetProperty("Position");
Size driverPositionArea = new Size(driverPosition.GetProperty("width").GetInt32(), FirstZoneSize.Height);
Point driverPositionPosition = new Point(driverPosition.GetProperty("x").GetInt32(), driverPosition.GetProperty("y").GetInt32());
var driverGapToLeader = windows[0].GetProperty("GapToLeader");
Size driverGapToLeaderArea = new Size(driverGapToLeader.GetProperty("width").GetInt32(), FirstZoneSize.Height);
Point driverGapToLeaderPosition = new Point(driverGapToLeader.GetProperty("x").GetInt32(), driverGapToLeader.GetProperty("y").GetInt32());
var driverLapTime = windows[0].GetProperty("LapTime");
Size driverLapTimeArea = new Size(driverLapTime.GetProperty("width").GetInt32(), FirstZoneSize.Height);
Point driverLapTimePosition = new Point(driverLapTime.GetProperty("x").GetInt32(), driverLapTime.GetProperty("y").GetInt32());
var driverDrs = windows[0].GetProperty("DRS");
Size driverDrsArea = new Size(driverDrs.GetProperty("width").GetInt32(), FirstZoneSize.Height);
Point driverDrsPosition = new Point(driverDrs.GetProperty("x").GetInt32(), driverDrs.GetProperty("y").GetInt32());
var driverTyres = windows[0].GetProperty("Tyres");
Size driverTyresArea = new Size(driverTyres.GetProperty("width").GetInt32(), FirstZoneSize.Height);
Point driverTyresPosition = new Point(driverTyres.GetProperty("x").GetInt32(), driverTyres.GetProperty("y").GetInt32());
var driverName = windows[0].GetProperty("Name");
Size driverNameArea = new Size(driverName.GetProperty("width").GetInt32(), FirstZoneSize.Height);
Point driverNamePosition = new Point(driverName.GetProperty("x").GetInt32(), driverName.GetProperty("y").GetInt32());
var driverSector1 = windows[0].GetProperty("Sector1");
Size driverSector1Area = new Size(driverSector1.GetProperty("width").GetInt32(), FirstZoneSize.Height);
Point driverSector1Position = new Point(driverSector1.GetProperty("x").GetInt32(), driverSector1.GetProperty("y").GetInt32());
var driverSector2 = windows[0].GetProperty("Sector2");
Size driverSector2Area = new Size(driverSector2.GetProperty("width").GetInt32(), FirstZoneSize.Height);
Point driverSector2Position = new Point(driverSector2.GetProperty("x").GetInt32(), driverSector2.GetProperty("y").GetInt32());
var driverSector3 = windows[0].GetProperty("Sector3");
Size driverSector3Area = new Size(driverSector3.GetProperty("width").GetInt32(), FirstZoneSize.Height);
Point driverSector3Position = new Point(driverSector3.GetProperty("x").GetInt32(), driverSector3.GetProperty("y").GetInt32());
float offset = (((float)mainZone.ZoneImage.Height - (float)(driverListToFill.Count * FirstZoneSize.Height)) / (float)driverListToFill.Count);
Bitmap MainZoneImage = mainZone.ZoneImage;
List<Zone> zonesToAdd = new List<Zone>();
List<Bitmap> zonesImages = new List<Bitmap>();
for (int i = 0; i < NUMBER_OF_DRIVERS; i++)
{
Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset));
Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize), "DriverZone");
zonesToAdd.Add(newDriverZone);
zonesImages.Add(newDriverZone.ZoneImage);
newDriverZone.ZoneImage.Save("Driver"+i+".png");
}
//Parallel.For(0, NUMBER_OF_DRIVERS, i =>
for (int i = 0; i < NUMBER_OF_DRIVERS; i++)
{
Zone newDriverZone = zonesToAdd[(int)i];
Bitmap zoneImg = zonesImages[(int)i];
newDriverZone.AddWindow(new DriverPositionWindow(zoneImg, new Rectangle(driverPositionPosition, driverPositionArea),LoadOCR));
newDriverZone.AddWindow(new DriverGapToLeaderWindow(zoneImg, new Rectangle(driverGapToLeaderPosition, driverGapToLeaderArea), LoadOCR));
newDriverZone.AddWindow(new DriverLapTimeWindow(zoneImg, new Rectangle(driverLapTimePosition, driverLapTimeArea), LoadOCR));
newDriverZone.AddWindow(new DriverDrsWindow(zoneImg, new Rectangle(driverDrsPosition, driverDrsArea), LoadOCR));
newDriverZone.AddWindow(new DriverTyresWindow(zoneImg, new Rectangle(driverTyresPosition, driverTyresArea), LoadOCR));
newDriverZone.AddWindow(new DriverNameWindow(zoneImg, new Rectangle(driverNamePosition, driverNameArea), LoadOCR));
newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector1Position, driverSector1Area),1, LoadOCR));
newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector2Position, driverSector2Area),2, LoadOCR));
newDriverZone.AddWindow(new DriverSectorWindow(zoneImg, new Rectangle(driverSector3Position, driverSector3Area),3, LoadOCR));
mainZone.AddZone(newDriverZone);
}//);
//MessageBox.Show("We have a main zone with " + MainZone.Zones.Count() + " Driver zones with " + MainZone.Zones[4].Windows.Count() + " windows each and we have " + Drivers.Count + " drivers");
mainZones.Add(mainZone);
}
}
catch (IOException ex)
{
MessageBox.Show("Error reading JSON file: " + ex.Message);
}
catch (JsonException ex)
{
MessageBox.Show("Invalid JSON format: " + ex.Message);
}
return mainZones;
}
/// <summary>
/// Method that calls all the zones and windows to get the content they can find on the image to display them
/// </summary>
/// <param name="idImage">The id of the image we are working with</param>
/// <returns>a string representation of all the returns</returns>
public async Task<string> Decode(List<Zone> mainZones,List<string> drivers)
{
string result = "";
List<DriverData> mainResults = new List<DriverData>();
//Decode
for (int mainZoneId = 0; mainZoneId < mainZones.Count; mainZoneId++)
{
switch (mainZoneId)
{
case 0:
//Main Zone
foreach (Zone z in mainZones[mainZoneId].Zones)
{
mainResults.Add(await z.Decode(Drivers));
}
break;
//Next there could be a Title Zone and TrackInfoZone
}
}
//Display
foreach (DriverData driver in mainResults)
{
result += driver.ToString();
result += Environment.NewLine;
}
return result;
}
/// <summary>
/// Method that can be used to convert an amount of miliseconds into a more readable human form
/// </summary>
/// <param name="amountOfMs">The given amount of miliseconds ton convert</param>
/// <returns>A human readable string that represents the ms</returns>
public static string ConvertMsToTime(int amountOfMs)
{
//Convert.ToInt32 would round upand I dont want that
int minuts = (int)((float)amountOfMs / (1000f * 60f));
int seconds = (int)((amountOfMs - (minuts * 60f * 1000f)) / 1000);
int ms = amountOfMs - ((minuts * 60 * 1000) + (seconds * 1000));
return minuts + ":" + seconds.ToString("00") + ":" + ms.ToString("000");
}
/// <summary>
/// Old method that can draw on an image where the windows and zones are created. mostly used for debugging
/// </summary>
/// <param name="idImage">the #id of the image we are working with</param>
/// <returns>the drawed bitmap</returns>
public Bitmap Draw(Bitmap image,List<Zone> mainZones)
{
Graphics g = Graphics.FromImage(image);
foreach (Zone z in mainZones)
{
int count = 0;
foreach (Zone zz in z.Zones)
{
g.DrawRectangle(Pens.Red, z.Bounds);
foreach (Window w in zz.Windows)
{
g.DrawRectangle(Pens.Blue, new Rectangle(z.Bounds.X + zz.Bounds.X, z.Bounds.Y + zz.Bounds.Y, zz.Bounds.Width, zz.Bounds.Height));
}
count++;
}
}
return image;
}
}
}
```
+420
View File
@@ -0,0 +1,420 @@
# 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;
namespace Test_Merge
{
public partial class Settings : Form
{
private string _grandPrixUrl = "";
private string _grandPrixName = "";
private int _grandPrixYear = 2000;
private List<string> _driverList = new List<string>();
private F1TVEmulator Emulator = null;
private ConfigurationTool Config = null;
private bool CreatingZone = false;
private Point ZoneP1;
private Point ZoneP2;
private bool CreatingWindow = false;
private Point WindowP1;
private Point WindowP2;
List<Rectangle> WindowsToAdd = new List<Rectangle>();
public string GrandPrixUrl { get => _grandPrixUrl; private set => _grandPrixUrl = value; }
public string GrandPrixName { get => _grandPrixName; private set => _grandPrixName = value; }
public int GrandPrixYear { get => _grandPrixYear; private set => _grandPrixYear = value; }
public List<string> DriverList { get => _driverList; private set => _driverList = value; }
public Settings()
{
InitializeComponent();
Load();
}
private void Load()
{
RefreshUI();
}
private void RefreshUI()
{
lsbDrivers.DataSource = null;
lsbDrivers.DataSource = DriverList;
if (Directory.Exists(ConfigurationTool.CONFIGS_FOLDER_NAME))
{
lsbPresets.DataSource = null;
lsbPresets.DataSource = Directory.GetFiles(ConfigurationTool.CONFIGS_FOLDER_NAME);
}
if (CreatingZone)
{
if (ZoneP1 == new Point(-1, -1))
{
lblZonePointsRemaning.Text = "2 points Remaining";
}
else
{
lblZonePointsRemaning.Text = "1 point Remaining";
}
}
else
{
lblZonePointsRemaning.Text = "";
}
if (CreatingWindow)
{
if (WindowP1 == new Point(-1, -1))
{
lblWindowPointsRemaining.Text = "2 points Remaining";
}
else
{
lblWindowPointsRemaining.Text = "1 point Remaining";
}
lblWindowPointsRemaining.Text = ConfigurationTool.NUMBER_OF_ZONES - WindowsToAdd.Count() + " Windows remaining";
}
else
{
lblWindowPointsRemaining.Text = "";
lblWindowsRemaining.Text = "";
}
if (Config != null)
{
pbxMain.Image = Config.MainZone.Draw();
if(Config.MainZone.Zones.Count > 0)
pbxDriverZone.Image = Config.MainZone.Zones[0].Draw();
}
}
private void CreateNewZone(Point p1, Point p2)
{
Rectangle dimensions = CreateAbsoluteRectangle(p1, p2);
Config = new ConfigurationTool((Bitmap)pbxMain.Image, dimensions);
RefreshUI();
}
private void CreateWindows(List<Rectangle> dimensions)
{
if (Config != null)
{
Config.AddWindows(dimensions);
}
}
private void tbxGpUrl_TextChanged(object sender, EventArgs e)
{
GrandPrixUrl = tbxGpUrl.Text;
}
private void tbxGpName_TextChanged(object sender, EventArgs e)
{
GrandPrixName = tbxGpName.Text;
}
private void tbxGpYear_TextChanged(object sender, EventArgs e)
{
int year;
try
{
year = Convert.ToInt32(tbxGpYear.Text);
}
catch
{
year = 1545;
}
GrandPrixYear = year;
}
private void btnAddDriver_Click(object sender, EventArgs e)
{
string newDriver = tbxDriverName.Text;
DriverList.Add(newDriver);
tbxDriverName.Text = "";
RefreshUI();
}
private void btnRemoveDriver_Click(object sender, EventArgs e)
{
if (lsbDrivers.SelectedIndex >= 0)
{
DriverList.RemoveAt(lsbDrivers.SelectedIndex);
}
RefreshUI();
}
private void SwitchZoneCreation()
{
if (CreatingZone)
{
CreatingZone = false;
lblZonePointsRemaning.Text = "";
}
else
{
CreatingZone = true;
if (Config != null)
Config.ResetMainZone();
if (CreatingWindow)
SwitchWindowCreation();
if (Emulator != null && Emulator.Ready)
{
Config = null;
pbxMain.Image = Emulator.Screenshot();
}
ZoneP1 = new Point(-1, -1);
ZoneP2 = new Point(-1, -1);
lblZonePointsRemaning.Text = "2 Points left";
}
RefreshUI();
}
private void SwitchWindowCreation()
{
if (CreatingWindow)
{
CreatingWindow = false;
}
else
{
CreatingWindow = true;
if (Config != null)
Config.ResetWindows();
if (CreatingZone)
SwitchZoneCreation();
WindowP1 = new Point(-1, -1);
WindowP2 = new Point(-1, -1);
WindowsToAdd = new List<Rectangle>();
}
RefreshUI();
}
private void btnCreatZone_Click(object sender, EventArgs e)
{
SwitchZoneCreation();
}
private void btnCreateWindow_Click(object sender, EventArgs e)
{
SwitchWindowCreation();
}
private void pbxMain_MouseClick(object sender, MouseEventArgs e)
{
if (CreatingZone && pbxMain.Image != null)
{
//Point coordinates = pbxMain.PointToClient(new Point(MousePosition.X, MousePosition.Y));
Point coordinates = e.Location;
float xOffset = (float)pbxMain.Image.Width / (float)pbxMain.Width;
float yOffset = (float)pbxMain.Image.Height / (float)pbxMain.Height;
Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset));
//MessageBox.Show("Coordinates" + Environment.NewLine + "Old : " + coordinates.ToString() + Environment.NewLine + "New : " + newPoint.ToString());
if (ZoneP1 == new Point(-1, -1))
{
ZoneP1 = newPoint;
}
else
{
ZoneP2 = newPoint;
CreateNewZone(ZoneP1, ZoneP2);
SwitchZoneCreation();
}
RefreshUI();
}
}
private void pbxMain_Click(object sender, EventArgs e)
{
//Not the right one to use visibly
}
private void pbxDriverZone_MouseClick(object sender, MouseEventArgs e)
{
if (CreatingWindow && pbxDriverZone.Image != null)
{
Point coordinates = e.Location;
float xOffset = (float)pbxDriverZone.Image.Width / (float)pbxDriverZone.Width;
float yOffset = (float)pbxDriverZone.Image.Height / (float)pbxDriverZone.Height;
Point newPoint = new Point(Convert.ToInt32((float)coordinates.X * xOffset), Convert.ToInt32((float)coordinates.Y * yOffset));
if (WindowP1 == new Point(-1, -1))
{
WindowP1 = newPoint;
}
else
{
WindowP2 = newPoint;
WindowsToAdd.Add(CreateAbsoluteRectangle(WindowP1, WindowP2));
if (WindowsToAdd.Count < ConfigurationTool.NUMBER_OF_ZONES)
{
WindowP1 = new Point(-1, -1);
WindowP2 = new Point(-1, -1);
}
else
{
WindowP1 = new Point(WindowP1.X, 0);
WindowP2 = new Point(WindowP2.X, pbxDriverZone.Image.Height);
CreateWindows(WindowsToAdd);
SwitchWindowCreation();
}
}
RefreshUI();
}
}
private void pbxDriverZone_Click(object sender, EventArgs e)
{
//Not the right one to use visibly
}
private Rectangle CreateAbsoluteRectangle(Point p1, Point p2)
{
Point newP1 = new Point();
Point newP2 = new Point();
if (p1.X < p2.X)
{
newP1.X = p1.X;
newP2.X = p2.X;
}
else
{
newP1.X = p2.X;
newP2.X = p1.X;
}
if (p1.Y < p2.Y)
{
newP1.Y = p1.Y;
newP2.Y = p2.Y;
}
else
{
newP1.Y = p2.Y;
newP2.Y = p1.Y;
}
return new Rectangle(newP1.X, newP1.Y, newP2.X - newP1.X, newP2.Y - newP1.Y);
}
private async void btnRefresh_Click(object sender, EventArgs e)
{
btnRefresh.Enabled = false;
if (Emulator == null || Emulator.GrandPrixUrl != tbxGpUrl.Text)
{
Emulator = new F1TVEmulator(tbxGpUrl.Text);
}
if (!Emulator.Ready)
{
Task<int> start = Task.Run(() => Emulator.Start());
int errorCode = await start;
if (errorCode != 0)
{
string message;
switch (errorCode)
{
case 101:
message = "Error " + errorCode + " Could not start the driver. It could be because an other instance is runnin make sure you closed them all before trying again";
break;
case 102:
message = "Error " + errorCode + " Could not navigate on the F1TV site. Make sure the correct URL has been given and that you logged from chrome. It can take a few minutes to update";
break;
case 103:
message = "Error " + errorCode + " The url is not a valid url";
break;
case 104:
message = "Error " + errorCode + " The url is not a valid url";
break;
case 105:
message = "Error " + errorCode + " There has been an error trying to emulate button presses. Please try again";
break;
case 106:
message = "Error " + errorCode + " There has been an error trying to emulate button presses. Please try again";
break;
default:
message = "Could not start the emulator Error " + errorCode;
break;
}
MessageBox.Show(message);
}
else
{
pbxMain.Image = Emulator.Screenshot();
}
}
else
{
pbxMain.Image = Emulator.Screenshot();
}
btnRefresh.Enabled = true;
}
private void Settings_FormClosing(object sender, FormClosingEventArgs e)
{
if (Emulator != null)
{
Emulator.Stop();
}
}
private void btnResetDriver_Click(object sender, EventArgs e)
{
if (Emulator != null)
{
Emulator.ResetDriver();
}
}
private void btnSavePreset_Click(object sender, EventArgs e)
{
string presetName = tbxPresetName.Text;
if (Config != null)
{
Config.SaveToJson(DriverList,presetName);
}
RefreshUI();
}
private void lsbPresets_SelectedIndexChanged(object sender, EventArgs e)
{
//Nothing
}
private void btnLoadPreset_Click(object sender, EventArgs e)
{
if (lsbPresets.SelectedIndex >= 0 && pbxMain.Image != null)
{
try
{
Reader reader = new Reader(lsbPresets.Items[lsbPresets.SelectedIndex].ToString(), (Bitmap)pbxMain.Image,false);
//MainZones #0 is the big main zone containing driver zones
Config = new ConfigurationTool((Bitmap)pbxMain.Image, reader.MainZones[0].Bounds);
Config.MainZone = reader.MainZones[0];
DriverList = reader.Drivers;
}
catch (Exception ex)
{
MessageBox.Show("Could not load the settings error :" + ex);
}
RefreshUI();
}
}
}
}
```
+322
View File
@@ -0,0 +1,322 @@
# Window.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 08/05/2023
/// File : Window.cs
/// Brief : Default Window object that is mainly expected to be inherited.
/// Version : 0.1
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
using System.IO;
using Tesseract;
using System.Text.RegularExpressions;
using System.Drawing.Drawing2D;
namespace Test_Merge
{
public class Window
{
private Rectangle _bounds;
private Bitmap _image;
private string _name;
protected TesseractEngine Engine;
public Rectangle Bounds { get => _bounds; private set => _bounds = value; }
public Bitmap Image { get => _image; set => _image = value; }
public string Name { get => _name; protected set => _name = value; }
//This will have to be changed if you want to make it run on your machine
public static DirectoryInfo TESS_DATA_FOLDER = new DirectoryInfo(@"C:\Users\Moi\Pictures\SeleniumScreens\TessData");
public Bitmap WindowImage
{
get
{
//This little trickery lets you have the image that the window sees
Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height);
Graphics g = Graphics.FromImage(sample);
g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel);
return sample;
}
}
public Window(Bitmap image, Rectangle bounds, bool generateEngine = true)
{
Image = image;
Bounds = bounds;
if (generateEngine)
{
Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, "eng", EngineMode.Default);
Engine.DefaultPageSegMode = PageSegMode.SingleLine;
}
}
/// <summary>
/// Method that will have to be used by the childrens to let the model make them decode the images they have
/// </summary>
/// <returns>Returns an object because we dont know what kind of return it will be</returns>
public virtual async Task<Object> DecodePng()
{
return "NaN";
}
/// <summary>
/// Method that will have to be used by the childrens to let the model make them decode the images they have
/// </summary>
/// <param name="driverList">This is a list of the different possible drivers in the race. It should not be too big but NEVER be too short</param>
/// <returns>Returns an object because we dont know what kind of return it will be</returns>
public virtual async Task<Object> DecodePng(List<string> driverList)
{
return "NaN";
}
/// <summary>
/// This converts an image into a byte[]. It can be usefull when doing unsafe stuff. Use at your own risks
/// </summary>
/// <param name="inputImage">The image you want to convert</param>
/// <returns>A byte array containing the image informations</returns>
public static byte[] ImageToByte(Image inputImage)
{
using (var stream = new MemoryStream())
{
inputImage.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
return stream.ToArray();
}
}
/// <summary>
/// This method is used to recover a time from a PNG using Tesseract OCR
/// </summary>
/// <param name="windowImage">The image where the text is</param>
/// <param name="windowType">The type of window it is</param>
/// <param name="Engine">The Tesseract Engine</param>
/// <returns>The time in milliseconds</returns>
public static async Task<int> GetTimeFromPng(Bitmap windowImage, OcrImage.WindowType windowType, TesseractEngine Engine)
{
//Kind of a big method but it has a lot of error handling and has to work with three special cases
string rawResult = "";
int result = 0;
switch (windowType)
{
case OcrImage.WindowType.Sector:
//The usual sector is in this form : 33.456
Engine.SetVariable("tessedit_char_whitelist", "0123456789.");
break;
case OcrImage.WindowType.LapTime:
//The usual Lap time is in this form : 1:45:345
Engine.SetVariable("tessedit_char_whitelist", "0123456789.:");
break;
case OcrImage.WindowType.Gap:
//The usual Gap is in this form : + 34.567
Engine.SetVariable("tessedit_char_whitelist", "0123456789.+");
break;
default:
Engine.SetVariable("tessedit_char_whitelist", "");
break;
}
Bitmap enhancedImage = new OcrImage(windowImage).Enhance(windowType);
var tessImage = Pix.LoadFromMemory(ImageToByte(enhancedImage));
Page page = Engine.Process(tessImage);
Graphics g = Graphics.FromImage(enhancedImage);
// Get the iterator for the page layout
using (var iter = page.GetIterator())
{
// Loop over the elements of the page layout
iter.Begin();
do
{
// Get the text for the current element
try
{
rawResult += iter.GetText(PageIteratorLevel.Word);
}
catch
{
//nothing we just dont add it if its not a number
}
} while (iter.Next(PageIteratorLevel.Word));
}
List<string> rawNumbers;
//In the gaps we can find '+' but we dont care about it its redondant a driver will never be - something
if (windowType == OcrImage.WindowType.Gap)
rawResult = Regex.Replace(rawResult, "[^0-9.:]", "");
//Splits into minuts seconds miliseconds
rawNumbers = rawResult.Split('.', ':').ToList<string>();
//removes any empty cells (tho this usually sign of a really bad OCR implementation tbh will have to be fixed higher in the chian)
rawNumbers.RemoveAll(x => ((string)x) == "");
if (rawNumbers.Count == 3)
{
//mm:ss:ms
result = (Convert.ToInt32(rawNumbers[0]) * 1000 * 60) + (Convert.ToInt32(rawNumbers[1]) * 1000) + Convert.ToInt32(rawNumbers[2]);
}
else
{
if (rawNumbers.Count == 2)
{
//ss:ms
result = (Convert.ToInt32(rawNumbers[0]) * 1000) + Convert.ToInt32(rawNumbers[1]);
if (result > 999999)
{
//We know that we have way too much seconds to make a minut
//Its usually because the ":" have been interpreted as a number
int minuts = (int)(rawNumbers[0][0] - '0');
// rawNumbers[0][1] should contain the : that has been mistaken
int seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
int ms = Convert.ToInt32(rawNumbers[1]);
result = (Convert.ToInt32(minuts) * 1000 * 60) + (Convert.ToInt32(seconds) * 1000) + Convert.ToInt32(ms);
}
}
else
{
if (rawNumbers.Count == 1)
{
try
{
result = Convert.ToInt32(rawNumbers[0]);
}
catch
{
//It can be because the input is empty or because its the LEADER bracket
result = 0;
}
}
else
{
//Auuuugh
result = 0;
}
}
}
page.Dispose();
return result;
}
/// <summary>
/// Method that recovers strings from an image using Tesseract OCR
/// </summary>
/// <param name="WindowImage">The image of the window that contains text</param>
/// <param name="Engine">The Tesseract engine</param>
/// <param name="allowedChars">The list of allowed chars</param>
/// <param name="windowType">The type of window the text is on. Depending on the context the OCR will behave differently</param>
/// <returns>the string it found</returns>
public static async Task<string> GetStringFromPng(Bitmap WindowImage, TesseractEngine Engine, string allowedChars = "", OcrImage.WindowType windowType = OcrImage.WindowType.Text)
{
string result = "";
Engine.SetVariable("tessedit_char_whitelist", allowedChars);
Bitmap rawData = WindowImage;
Bitmap enhancedImage = new OcrImage(rawData).Enhance(windowType);
Page page = Engine.Process(enhancedImage);
using (var iter = page.GetIterator())
{
iter.Begin();
do
{
result += iter.GetText(PageIteratorLevel.Word);
} while (iter.Next(PageIteratorLevel.Word));
}
page.Dispose();
return result;
}
/// <summary>
/// Get a smaller image from a bigger one
/// </summary>
/// <param name="inputBitmap">The big bitmap you want to get a part of</param>
/// <param name="newBitmapDimensions">The dimensions of the new bitmap</param>
/// <returns>The little bitmap</returns>
protected Bitmap GetSmallBitmapFromBigOne(Bitmap inputBitmap, Rectangle newBitmapDimensions)
{
Bitmap sample = new Bitmap(newBitmapDimensions.Width, newBitmapDimensions.Height);
Graphics g = Graphics.FromImage(sample);
g.DrawImage(inputBitmap, new Rectangle(0, 0, sample.Width, sample.Height), newBitmapDimensions, GraphicsUnit.Pixel);
return sample;
}
/// <summary>
/// Returns the closest string from a list of options
/// </summary>
/// <param name="options">an array of all the possibilities</param>
/// <param name="testString">the string you want to compare</param>
/// <returns>The closest option</returns>
protected static string FindClosestMatch(List<string> options, string testString)
{
var closestMatch = "";
var closestDistance = int.MaxValue;
foreach (var item in options)
{
var distance = LevenshteinDistance(item, testString);
if (distance < closestDistance)
{
closestMatch = item;
closestDistance = distance;
}
}
return closestMatch;
}
//This method has been generated with the help of ChatGPT
/// <summary>
/// Method that computes a score of distance between two strings
/// </summary>
/// <param name="string1">The first string (order irrelevant)</param>
/// <param name="string2">The second string (order irrelevant)</param>
/// <returns>The levenshtein distance</returns>
protected static int LevenshteinDistance(string string1, string string2)
{
if (string.IsNullOrEmpty(string1))
{
return string.IsNullOrEmpty(string2) ? 0 : string2.Length;
}
if (string.IsNullOrEmpty(string2))
{
return string.IsNullOrEmpty(string1) ? 0 : string1.Length;
}
var d = new int[string1.Length + 1, string2.Length + 1];
for (var i = 0; i <= string1.Length; i++)
{
d[i, 0] = i;
}
for (var j = 0; j <= string2.Length; j++)
{
d[0, j] = j;
}
for (var i = 1; i <= string1.Length; i++)
{
for (var j = 1; j <= string2.Length; j++)
{
var cost = (string1[i - 1] == string2[j - 1]) ? 0 : 1;
d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost);
}
}
return d[string1.Length, string2.Length];
}
public virtual string ToJSON()
{
string result = "";
result += "\"" + Name + "\"" + ":{" + Environment.NewLine;
result += "\t" + "\"x\":" + Bounds.X + "," + Environment.NewLine;
result += "\t" + "\"y\":" + Bounds.Y + "," + Environment.NewLine;
result += "\t" + "\"width\":" + Bounds.Width + Environment.NewLine;
result += "}";
return result;
}
}
}
```
+242
View File
@@ -0,0 +1,242 @@
# Zone.cs
``` cs
/// Author : Maxime Rohmer
/// Date : 08/05/2023
/// File : Zone.cs
/// Brief : Class that contains all the methods and infos for a zone. This is designed to be potentially be inherited.
/// Version : 0.1
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Test_Merge
{
public class Zone
{
private Rectangle _bounds;
private List<Zone> _zones;
private List<Window> _windows;
private Bitmap _image;
private string _name;
public Bitmap ZoneImage
{
get
{
//This little trickery lets you have the image that the zone sees
Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height);
Graphics g = Graphics.FromImage(sample);
g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel);
return sample;
}
}
public Bitmap Image
{
get { return _image; }
set
{
//It automatically sets the image for the contained windows and zones
_image = Image;
foreach (Window w in Windows)
{
w.Image = ZoneImage;
}
foreach (Zone z in Zones)
{
z.Image = Image;
}
}
}
public Rectangle Bounds { get => _bounds; protected set => _bounds = value; }
public List<Zone> Zones { get => _zones; protected set => _zones = value; }
public List<Window> Windows { get => _windows; protected set => _windows = value; }
public string Name { get => _name; protected set => _name = value; }
public Zone(Bitmap image, Rectangle bounds, string name)
{
Windows = new List<Window>();
Zones = new List<Zone>();
Name = name;
//You cant set the image in the CTOR because the processing is impossible at first initiation
_image = image;
Bounds = bounds;
}
/// <summary>
/// Adds a zone to the list of zones
/// </summary>
/// <param name="zone">The zone you want to add</param>
public virtual void AddZone(Zone zone)
{
Zones.Add(zone);
}
/// <summary>
/// Add a window to the list of windows
/// </summary>
/// <param name="window">the window you want to add</param>
public virtual void AddWindow(Window window)
{
Windows.Add(window);
}
/// <summary>
/// Calls all the windows to do OCR and to give back the results so we can send them to the model
/// </summary>
/// <param name="driverList">A list of all the driver in the race to help with text recognition</param>
/// <returns>A driver data object that contains all the infos about a driver</returns>
public virtual async Task<DriverData> Decode(List<string> driverList)
{
int sectorCount = 0;
DriverData result = new DriverData();
Parallel.ForEach(Windows, async w =>
{
// A switch would be prettier but I dont think its supported in this C# version
if (w is DriverNameWindow)
result.Name = (string)await (w as DriverNameWindow).DecodePng(driverList);
if (w is DriverDrsWindow)
result.DRS = (bool)await (w as DriverDrsWindow).DecodePng();
if (w is DriverGapToLeaderWindow)
result.GapToLeader = (int)await (w as DriverGapToLeaderWindow).DecodePng();
if (w is DriverLapTimeWindow)
result.LapTime = (int)await (w as DriverLapTimeWindow).DecodePng();
if (w is DriverPositionWindow)
result.Position = (int)await (w as DriverPositionWindow).DecodePng();
if (w is DriverSectorWindow)
{
sectorCount++;
if (sectorCount == 1)
result.Sector1 = (int)await (w as DriverSectorWindow).DecodePng();
if (sectorCount == 2)
result.Sector2 = (int)await (w as DriverSectorWindow).DecodePng();
if (sectorCount == 3)
result.Sector3 = (int)await (w as DriverSectorWindow).DecodePng();
}
if (w is DriverTyresWindow)
result.CurrentTyre = (Tyre)await (w as DriverTyresWindow).DecodePng();
});
return result;
}
public virtual Bitmap Draw()
{
Bitmap img;
//If its the main zone we want to see everything
if (Zones.Count > 0)
{
img = Image;
}
else
{
img = ZoneImage;
}
Graphics g = Graphics.FromImage(img);
//If its the main zone we need to visualize the Zone bounds displayed
if (Zones.Count > 0)
g.DrawRectangle(new Pen(Brushes.Violet, 5), Bounds);
foreach (Zone z in Zones)
{
Rectangle newBounds = new Rectangle(z.Bounds.X, z.Bounds.Y + Bounds.Y, z.Bounds.Width, z.Bounds.Height);
g.DrawRectangle(Pens.Red, newBounds);
}
foreach (Window w in Windows)
{
g.DrawRectangle(Pens.Blue, w.Bounds);
}
return img;
}
public void ResetZones()
{
Zones.Clear();
}
public void ResetWindows()
{
foreach (Zone z in Zones)
{
z.ResetWindows();
}
Windows.Clear();
}
public virtual string ToJSON()
{
string result = "";
result += "\"" + Name + "\":{" + Environment.NewLine;
result += "\t" + "\"x\":" + Bounds.X + "," + Environment.NewLine;
result += "\t" + "\"y\":" + Bounds.Y + "," + Environment.NewLine;
result += "\t" + "\"width\":" + Bounds.Width + "," + Environment.NewLine;
result += "\t" + "\"height\":" + Bounds.Height;
if (Windows.Count != 0)
{
result += "," + Environment.NewLine;
result += "\t" + "\"Windows\":[" + Environment.NewLine;
result += "\t\t{" + Environment.NewLine;
int Wcount = 0;
foreach (Window w in Windows)
{
result += "\t\t" + w.ToJSON();
Wcount++;
if (Wcount != Windows.Count)
result += ",";
}
result += "\t\t}" + Environment.NewLine;
result += "\t" + "]" + Environment.NewLine;
}
else
{
result += Environment.NewLine;
}
if (Zones.Count != 0)
{
result += "," + Environment.NewLine;
result += "\t" + "\"Zones\":[" + Environment.NewLine;
result += "\t\t{" + Environment.NewLine;
int Zcount = 0;
//foreach (Zone z in Zones)
//{
result += "\t\t" + Zones[0].ToJSON();
Zcount++;
if (Zcount != Zones.Count)
//result += ",";
//}
result += "\t\t}" + Environment.NewLine;
result += "\t" + "]" + Environment.NewLine;
}
else
{
result += Environment.NewLine;
}
result += "}";
return result;
}
/// <summary>
/// Checks if the given Rectangle fits in the current zone
/// </summary>
/// <param name="InputRectangle">The Rectangle you want to check the fittment</param>
/// <returns></returns>
protected bool Fits(Rectangle inputRectangle)
{
if (inputRectangle.X + inputRectangle.Width > Bounds.Width || inputRectangle.Y + inputRectangle.Height > Bounds.Height || inputRectangle.X < 0 || inputRectangle.Y < 0)
{
return false;
}
else
{
return true;
}
}
}
}
```
+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")
```