32 KiB
32 KiB
Window.cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : Window.cs
/// Brief : Default Window object that is mainly expected to be inherited.
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
using System.IO;
using Tesseract;
using System.Text.RegularExpressions;
using System.Drawing.Drawing2D;
namespace TrackTrends
{
public class Window
{
public const string STRING_DEBUG_FOLDER = "./GetString";
public const string LAPTIME_DEBUG_FOLDER = "./LapTime";
public const string GAPTOLEADER_DEBUG_FOLDER = "./Gap";
public const string SECTOR1_DEBUG_FOLDER = "./Sector1";
public const string SECTOR2_DEBUG_FOLDER = "./Sector2";
public const string SECTOR3_DEBUG_FOLDER = "./Sector3";
public const string DRS_DEBUG_FOLDER = "./DRS";
public const string TYRE_DEBUG_FOLDER = "./Tyre";
private Rectangle _bounds;
private Bitmap _image;
private string _name;
protected TesseractEngine Engine;
public Rectangle Bounds { get => _bounds; private set => _bounds = value; }
public Bitmap Image { get => _image; set => _image = value; }
public string Name { get => _name; protected set => _name = value; }
//This will have to be changed if you want to make it run on your machine
public static DirectoryInfo TESS_DATA_FOLDER = new DirectoryInfo(@"C:\Users\Moi\Pictures\SeleniumScreens\TessData");
//Debug
public static Random rnd = new Random();
public Bitmap WindowImage
{
get
{
//This little trickery lets you have the image that the window sees
Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height);
Graphics g = Graphics.FromImage(sample);
g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel);
return sample;
}
}
/// <summary>
/// Creates a new Window
/// </summary>
/// <param name="image">The image of the parent zone</param>
/// <param name="bounds">The position and size of the window</param>
/// <param name="generateEngine">Does the window need to generate a tesseract engine (takes time and ressources)</param>
public Window(Bitmap image, Rectangle bounds, bool generateEngine = true)
{
Image = image;
Bounds = bounds;
if (generateEngine)
{
Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, "eng", EngineMode.Default);
Engine.DefaultPageSegMode = PageSegMode.SingleLine;
}
//DEBUG
/*
if (!Directory.Exists(STRING_DEBUG_FOLDER))
Directory.CreateDirectory(STRING_DEBUG_FOLDER);
if (!Directory.Exists(LAPTIME_DEBUG_FOLDER))
Directory.CreateDirectory(LAPTIME_DEBUG_FOLDER);
if (!Directory.Exists(GAPTOLEADER_DEBUG_FOLDER))
Directory.CreateDirectory(GAPTOLEADER_DEBUG_FOLDER);
if (!Directory.Exists(SECTOR1_DEBUG_FOLDER))
Directory.CreateDirectory(SECTOR1_DEBUG_FOLDER);
if (!Directory.Exists(SECTOR2_DEBUG_FOLDER))
Directory.CreateDirectory(SECTOR2_DEBUG_FOLDER);
if (!Directory.Exists(SECTOR3_DEBUG_FOLDER))
Directory.CreateDirectory(SECTOR3_DEBUG_FOLDER);
if (!Directory.Exists(DRS_DEBUG_FOLDER))
Directory.CreateDirectory(DRS_DEBUG_FOLDER);
if (!Directory.Exists(TYRE_DEBUG_FOLDER))
Directory.CreateDirectory(TYRE_DEBUG_FOLDER);
*/
}
/// <summary>
/// Method that will have to be used by the childrens to let the model make them decode the images they have
/// </summary>
/// <returns>Returns an object because we dont know what kind of return it will be</returns>
public virtual Object DecodePng()
{
return "NaN";
}
/// <summary>
/// Method that will have to be used by the childrens to let the model make them decode the images they have
/// </summary>
/// <param name="driverList">This is a list of the different possible drivers in the race. It should not be too big but NEVER be too short</param>
/// <returns>Returns an object because we dont know what kind of return it will be</returns>
public virtual Object DecodePng(List<string> driverList)
{
return "NaN";
}
/// <summary>
/// This converts an image into a byte[]. It can be usefull when doing unsafe stuff. Use at your own risks
/// </summary>
/// <param name="inputImage">The image you want to convert</param>
/// <returns>A byte array containing the image informations</returns>
public static byte[] ImageToByte(Image inputImage)
{
using (var stream = new MemoryStream())
{
inputImage.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
return stream.ToArray();
}
}
/// <summary>
/// This method is used to recover a time from a PNG using Tesseract OCR
/// </summary>
/// <param name="windowImage">The image where the text is</param>
/// <param name="windowType">The type of window it is</param>
/// <param name="Engine">The Tesseract Engine</param>
/// <returns>The time in milliseconds</returns>
public static int GetTimeFromPng(Bitmap image, OcrImage.WindowType windowType, TesseractEngine Engine)
{
//Kind of a big method but it has a lot of error handling and has to work with three special cases
string rawResult = "";
int result = 0;
//Debug
int salt = rnd.Next(0, 999999);
switch (windowType)
{
case OcrImage.WindowType.Sector:
//The usual sector is in this form : 33.456
Engine.SetVariable("tessedit_char_whitelist", "0123456789.");
break;
case OcrImage.WindowType.LapTime:
//The usual Lap time is in this form : 1:45:345
Engine.SetVariable("tessedit_char_whitelist", "0123456789.:");
break;
case OcrImage.WindowType.Gap:
//The usual Gap is in this form : + 34.567
Engine.SetVariable("tessedit_char_whitelist", "0123456789.+");
break;
default:
Engine.SetVariable("tessedit_char_whitelist", "");
break;
}
Bitmap enhancedImage = new OcrImage(image).Enhance(windowType);
var tessImage = Pix.LoadFromMemory(ImageToByte(enhancedImage));
Page page = Engine.Process(tessImage);
Graphics g = Graphics.FromImage(enhancedImage);
// Get the iterator for the page layout
using (var iter = page.GetIterator())
{
// Loop over the elements of the page layout
iter.Begin();
do
{
// Get the text for the current element
try
{
rawResult += iter.GetText(PageIteratorLevel.Word);
}
catch
{
//nothing we just dont add it if its not a number
}
} while (iter.Next(PageIteratorLevel.Word));
}
List<string> rawNumbers;
//In the gaps we can find '+' but we dont care about it its redondant a driver will never be - something
if (windowType == OcrImage.WindowType.Gap)
rawResult = Regex.Replace(rawResult, "[^0-9.:]", "");
//Splits into minuts seconds miliseconds
rawNumbers = rawResult.Split('.', ':').ToList<string>();
//removes any empty cells (tho this usually sign of a really bad OCR implementation tbh will have to be fixed higher in the chian)
rawNumbers.RemoveAll(x => ((string)x) == "");
int minuts = 0;
int seconds = 0;
int miliseconds = 0;
switch (windowType)
{
case OcrImage.WindowType.Sector:
//Usually there is supposed to be only 2 parts.
if (rawNumbers.Count == 2)
{
//The perect case
try
{
seconds = Convert.ToInt32(rawNumbers[0].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1].ToString());
}
catch
{
Console.WriteLine("Sector time convertion failed");
}
}
else
{
if (rawNumbers.Count == 1)
{
//Here it is a little harder... Usually its because a '.' has been overlooked or interpreted as a number
if (rawNumbers[0].Length == 6)
{
//The '.' has been understood as a number
try
{
seconds = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString());
miliseconds = Convert.ToInt32(rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString() + rawNumbers[0][5].ToString());
}
catch
{
Console.WriteLine("Sector time convertion failed");
}
}
else
{
if (rawNumbers[0].Length == 5)
{
//The '.' has been overlooked
try
{
seconds = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString());
miliseconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString());
}
catch
{
Console.WriteLine("Sector time convertion failed");
}
}
else
{
Console.WriteLine("Sector time convertion failed");
}
}
}
else
{
//The OCR detected more than 1 '.' wich is concerning because that means that something went really wrong
Console.WriteLine("Sector time convertion failed");
}
}
result = 0;
result += seconds * 1000;
result += miliseconds;
break;
case OcrImage.WindowType.LapTime:
if (rawNumbers.Count == 3)
{
//The normal way
try
{
minuts = Convert.ToInt32(rawNumbers[0].ToString());
seconds = Convert.ToInt32(rawNumbers[1].ToString());
miliseconds = Convert.ToInt32(rawNumbers[2].ToString());
}
catch
{
Console.WriteLine("Lap time convertion failed");
}
}
else
{
if (rawNumbers.Count == 2)
{
//Either the ':' or the '.' has been missinterpreted
if (rawNumbers[0].Length > rawNumbers[1].Length)
{
//The ':' has been missinterpreted
if (rawNumbers[0].Length == 3)
{
//It has been forgotten
try
{
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
seconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1]);
}
catch
{
Console.WriteLine("Lap time convertion failed");
}
}
else
{
if (rawNumbers[0].Length == 4)
{
//I has been translated into an other number
try
{
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1]);
}
catch
{
Console.WriteLine("Lap time convertion failed");
}
}
else
{
//This could happen if the ':' has been missinterpreted with a lap time of over 9 minuts (HIGLY IMPROBABLE)
Console.WriteLine("Lap time convertion failed");
}
}
}
else
{
//The '.' has been missinterpreted
if (rawNumbers[1].Length == 5)
{
//It has been forgotten
minuts = Convert.ToInt32(rawNumbers[0].ToString());
seconds = Convert.ToInt32(rawNumbers[1][0].ToString() + rawNumbers[1][1].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1][2].ToString() + rawNumbers[1][3].ToString() + rawNumbers[1][4].ToString());
}
else
{
if (rawNumbers[1].Length == 6)
{
try
{
//It has been interpreted as a number
minuts = Convert.ToInt32(rawNumbers[0].ToString());
seconds = Convert.ToInt32(rawNumbers[1][0].ToString() + rawNumbers[1][1].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1][3].ToString() + rawNumbers[1][4].ToString() + rawNumbers[1][5].ToString());
}
catch
{
//It can happen and to be honest I dont know how to fix it
}
}
else
{
Console.WriteLine("Lap time convertion failed");
}
}
}
}
else
{
if (rawNumbers.Count == 1)
{
//Both the '.' and the ':' have been missinterpreted
if (rawNumbers[0].Length == 6)
{
//The just all have been forgotten
try
{
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
seconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
miliseconds = Convert.ToInt32(rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString() + rawNumbers[0][5].ToString());
}
catch
{
Console.WriteLine("Lap time convertion failed");
}
}
else
{
if (rawNumbers[0].Length == 7)
{
//The '.' or ':' have been interpreted as a number (usually the ':')
try
{
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
miliseconds = Convert.ToInt32(rawNumbers[0][4].ToString() + rawNumbers[0][5].ToString() + rawNumbers[0][6].ToString());
}
catch
{
Console.WriteLine("Lap time convertion failed");
}
}
else
{
if (rawNumbers[0].Length == 8)
{
//Both have been interpreted as a number
try
{
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
miliseconds = Convert.ToInt32(rawNumbers[0][5].ToString() + rawNumbers[0][6].ToString() + rawNumbers[0][7].ToString());
}
catch
{
Console.WriteLine("Lap time convertion failed");
}
}
else
{
//I dont know what could have happened
Console.WriteLine("Lap time convertion failed");
}
}
}
}
else
{
//I dont know what could have happened
Console.WriteLine("Lap time convertion failed");
}
}
}
result = 0;
result += minuts * 60 * 1000;
result += seconds * 1000;
result += miliseconds;
break;
case OcrImage.WindowType.Gap:
if (rawNumbers.Count == 2)
{
// This should be the x.xxx or a missed x:xx.xxx
if (rawNumbers[0].Length > 2)
{
//Its a missed x:xx.xxx
if (rawNumbers[0].Length == 3)
{
//It forgot the ":"
try
{
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
seconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1]);
}
catch
{
Console.WriteLine("Gap to leader convertion failed");
}
}
else
{
//The ":" has been mistaken as a number
if (rawNumbers[0].Length == 4)
{
try
{
minuts = Convert.ToInt32(rawNumbers[0][0].ToString());
seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1]);
}
catch
{
Console.WriteLine("Gap to leader convertion failed");
}
}
else
{
Console.WriteLine("Gap to leader convertion failed");
}
}
}
else
{
//It should be a normal x.xxx or xx.xxx
try
{
seconds = Convert.ToInt32(rawNumbers[0].ToString());
miliseconds = Convert.ToInt32(rawNumbers[1].ToString());
}
catch
{
Console.WriteLine("Gap to leader convertion failed");
}
}
}
else
{
if (rawNumbers.Count == 1)
{
//can be anything depending on the size of the string
if (rawNumbers[0].Length == 4)
{
//We just missed the '.'
try
{
seconds = Convert.ToInt32(rawNumbers[0][0].ToString());
miliseconds = Convert.ToInt32(rawNumbers[0][1].ToString() + rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
}
catch
{
Console.WriteLine("Gap to leader convertion failed");
}
}
else
{
if (rawNumbers[0].Length == 5)
{
//We just missed the '.'
try
{
seconds = Convert.ToInt32(rawNumbers[0][0].ToString() + rawNumbers[0][1].ToString());
miliseconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString() + rawNumbers[0][4].ToString());
}
catch
{
Console.WriteLine("Gap to leader convertion failed");
}
}
//There is just too much possibilities that it would be stupid to try and tell them appart so for now im leaving that as just an error
Console.WriteLine("Gap to leader convertion failed");
}
}
else
{
if (rawNumbers.Count == 3)
{
// This should be the x:xx.xxx
try
{
//Gaps cant be more than 9 minuts so if there is more than 1 digit it means that the '+' has been understood as an other number
if (rawNumbers[0].Length > 1)
rawNumbers[0] = rawNumbers[0][rawNumbers[0].Length - 1].ToString();
minuts = Convert.ToInt32(rawNumbers[0].ToString());
seconds = Convert.ToInt32(rawNumbers[1].ToString());
miliseconds = Convert.ToInt32(rawNumbers[2].ToString());
}
catch
{
Console.WriteLine("Gap to leader convertion failed");
}
}
}
}
result = 0;
result += minuts * 60 * 1000;
result += seconds * 1000;
result += miliseconds;
break;
default:
try
{
result = Convert.ToInt32(rawNumbers[0].ToString());
}
catch
{
result = 0;
}
break;
}
page.Dispose();
return result;
}
/// <summary>
/// Method that recovers strings from an image using Tesseract OCR
/// </summary>
/// <param name="WindowImage">The image of the window that contains text</param>
/// <param name="Engine">The Tesseract engine</param>
/// <param name="allowedChars">The list of allowed chars</param>
/// <param name="windowType">The type of window the text is on. Depending on the context the OCR will behave differently</param>
/// <returns>the string it found</returns>
public static string GetStringFromPng(Bitmap image, TesseractEngine Engine, string allowedChars = "", OcrImage.WindowType windowType = OcrImage.WindowType.Text)
{
string result = "";
//Debug
int salt = rnd.Next(0, 999999);
Engine.SetVariable("tessedit_char_whitelist", allowedChars);
Bitmap rawData = image;
Bitmap enhancedImage = new OcrImage(rawData).Enhance(windowType);
Page page = Engine.Process(enhancedImage);
using (var iter = page.GetIterator())
{
iter.Begin();
do
{
result += iter.GetText(PageIteratorLevel.Word);
} while (iter.Next(PageIteratorLevel.Word));
}
page.Dispose();
return result;
}
/// <summary>
/// Get a smaller image from a bigger one
/// </summary>
/// <param name="inputBitmap">The big bitmap you want to get a part of</param>
/// <param name="newBitmapDimensions">The dimensions of the new bitmap</param>
/// <returns>The little bitmap</returns>
protected Bitmap GetSmallBitmapFromBigOne(Bitmap inputBitmap, Rectangle newBitmapDimensions)
{
Bitmap sample = new Bitmap(newBitmapDimensions.Width, newBitmapDimensions.Height);
Graphics g = Graphics.FromImage(sample);
g.DrawImage(inputBitmap, new Rectangle(0, 0, sample.Width, sample.Height), newBitmapDimensions, GraphicsUnit.Pixel);
return sample;
}
/// <summary>
/// Returns the closest string from a list of options
/// </summary>
/// <param name="options">an array of all the possibilities</param>
/// <param name="testString">the string you want to compare</param>
/// <returns>The closest option</returns>
protected static string FindClosestMatch(List<string> options, string testString)
{
var closestMatch = "";
var closestDistance = int.MaxValue;
foreach (var item in options)
{
var distance = LevenshteinDistance(item, testString);
if (distance < closestDistance)
{
closestMatch = item;
closestDistance = distance;
}
}
return closestMatch;
}
//This method has been generated with the help of ChatGPT
/// <summary>
/// Method that computes a score of distance between two strings
/// </summary>
/// <param name="string1">The first string (order irrelevant)</param>
/// <param name="string2">The second string (order irrelevant)</param>
/// <returns>The levenshtein distance</returns>
protected static int LevenshteinDistance(string string1, string string2)
{
if (string.IsNullOrEmpty(string1))
{
return string.IsNullOrEmpty(string2) ? 0 : string2.Length;
}
if (string.IsNullOrEmpty(string2))
{
return string.IsNullOrEmpty(string1) ? 0 : string1.Length;
}
var d = new int[string1.Length + 1, string2.Length + 1];
for (var i = 0; i <= string1.Length; i++)
{
d[i, 0] = i;
}
for (var j = 0; j <= string2.Length; j++)
{
d[0, j] = j;
}
for (var i = 1; i <= string1.Length; i++)
{
for (var j = 1; j <= string2.Length; j++)
{
var cost = (string1[i - 1] == string2[j - 1]) ? 0 : 1;
d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost);
}
}
return d[string1.Length, string2.Length];
}
}
}