# Window.cs ``` cs /// Author : Maxime Rohmer /// Date : 30/05/2023 /// File : Window.cs /// Brief : Default Window object that is mainly expected to be inherited. /// Version : Alpha 1.0 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; using System.IO; using Tesseract; using System.Text.RegularExpressions; using System.Drawing.Drawing2D; namespace TrackTrends { public class Window { public const string STRING_DEBUG_FOLDER = "./GetString"; public const string LAPTIME_DEBUG_FOLDER = "./LapTime"; public const string GAPTOLEADER_DEBUG_FOLDER = "./Gap"; public const string SECTOR1_DEBUG_FOLDER = "./Sector1"; public const string SECTOR2_DEBUG_FOLDER = "./Sector2"; public const string SECTOR3_DEBUG_FOLDER = "./Sector3"; public const string DRS_DEBUG_FOLDER = "./DRS"; public const string TYRE_DEBUG_FOLDER = "./Tyre"; private Rectangle _bounds; private Bitmap _image; private string _name; protected TesseractEngine Engine; public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Bitmap Image { get => _image; set => _image = value; } public string Name { get => _name; protected set => _name = value; } //This will have to be changed if you want to make it run on your machine public static DirectoryInfo TESS_DATA_FOLDER = new DirectoryInfo(@"C:\Users\Moi\Pictures\SeleniumScreens\TessData"); //Debug public static Random rnd = new Random(); public Bitmap WindowImage { get { //This little trickery lets you have the image that the window sees Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Graphics g = Graphics.FromImage(sample); g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); return sample; } } /// /// Creates a new Window /// /// The image of the parent zone /// The position and size of the window /// Does the window need to generate a tesseract engine (takes time and ressources) 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); */ } /// /// Method that will have to be used by the childrens to let the model make them decode the images they have /// /// Returns an object because we dont know what kind of return it will be public virtual Object DecodePng() { return "NaN"; } /// /// Method that will have to be used by the childrens to let the model make them decode the images they have /// /// This is a list of the different possible drivers in the race. It should not be too big but NEVER be too short /// Returns an object because we dont know what kind of return it will be public virtual Object DecodePng(List driverList) { return "NaN"; } /// /// This converts an image into a byte[]. It can be usefull when doing unsafe stuff. Use at your own risks /// /// The image you want to convert /// A byte array containing the image informations public static byte[] ImageToByte(Image inputImage) { using (var stream = new MemoryStream()) { inputImage.Save(stream, System.Drawing.Imaging.ImageFormat.Png); return stream.ToArray(); } } /// /// This method is used to recover a time from a PNG using Tesseract OCR /// /// The image where the text is /// The type of window it is /// The Tesseract Engine /// The time in milliseconds 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 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(); //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; } /// /// Method that recovers strings from an image using Tesseract OCR /// /// The image of the window that contains text /// The Tesseract engine /// The list of allowed chars /// The type of window the text is on. Depending on the context the OCR will behave differently /// the string it found 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; } /// /// Get a smaller image from a bigger one /// /// The big bitmap you want to get a part of /// The dimensions of the new bitmap /// The little bitmap 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; } /// /// Returns the closest string from a list of options /// /// an array of all the possibilities /// the string you want to compare /// The closest option protected static string FindClosestMatch(List 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 /// /// Method that computes a score of distance between two strings /// /// The first string (order irrelevant) /// The second string (order irrelevant) /// The levenshtein distance 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]; } } } ```