/// 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 { 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; } } 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 async Task 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 async Task 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 async Task 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(salt,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) == ""); switch (windowType) { case OcrImage.WindowType.Sector: int seconds = 0; int miliseconds = 0; //Usually there is supposed to be only 2 parts. if (rawNumbers.Count == 2) { //The perect case try { seconds = Convert.ToInt32(rawNumbers[0]); miliseconds = Convert.ToInt32(rawNumbers[1]); } 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: break; case OcrImage.WindowType.Gap: break; default: try { result = Convert.ToInt32(rawNumbers[0]); } 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 async Task 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(salt,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]; } } }