Compare commits

25 Commits

Author SHA1 Message Date
Maxluli 4b9592c8e1 cleaned the rest of the files 2023-04-25 15:52:44 +02:00
Maxluli f2761db4c9 Cleaned some more files 2023-04-25 15:43:13 +02:00
Maxluli 57f19a1438 Cleaned Zone.cs 2023-04-25 15:22:11 +02:00
Maxluli 32b20ff317 Cleaned Window.cs 2023-04-25 15:16:08 +02:00
Maxluli 7cb27ca69c Cleaned Reader.cs 2023-04-25 14:49:13 +02:00
Maxluli a784cef87f Cleaned the OcrImage.cs 2023-04-25 14:00:00 +02:00
Maxluli fa07045199 Slightly improved performances or the tyre recognition 2023-04-25 11:06:03 +02:00
Maxluli 274cf48f37 Slightly improved performances of the tyre recognition and added some debug tools 2023-04-25 10:46:06 +02:00
Maxluli 13481445fa Just added some debug image saving to help debug and make doc 2023-04-24 13:58:57 +02:00
Maxluli 997f6407f6 Improved processing quality of the tyres number of laps 2023-04-24 13:47:12 +02:00
Maxluli 70085721f4 Simplified the tyre detection and improved its accuracy 2023-04-24 13:35:54 +02:00
Maxluli 5f8f1f0704 Fixed the ridiculus times where the ':' in laptimes was interpreted as a number 2023-04-13 07:30:05 +02:00
Maxluli dbda3d3788 LapTime recognition has improved a lot 2023-04-13 06:37:33 +02:00
Maxluli c8359c074c Tried to paralellize the loading but it stalls the programm so I reverted it 2023-04-11 11:44:41 +02:00
Maxluli bc18274762 'Fixed' ramping memory consumption 2023-04-11 11:11:55 +02:00
Maxluli 0c84d468b2 Now the loading is trash but the OCR is starting to be fast 2023-04-11 11:05:25 +02:00
Maxluli fde3bcaae6 Increased OCR performances 2023-04-11 10:44:29 +02:00
Maxluli 8796fed916 Now everything has been converted to asynchronus 2023-04-11 09:40:21 +02:00
Maxluli e278a1c006 Loading is now 50X faster 2023-04-10 11:08:32 +02:00
Maxluli 4d03070849 Added diagnostic tools 2023-04-10 10:21:21 +02:00
Maxluli f6fdc8b150 Letters are not recognized way better 2023-04-10 09:37:19 +02:00
Maxluli ea3ca8cd28 Tyres FINALLY are starting to get recognized properly 2023-04-08 09:21:24 +02:00
Maxluli 08a2176385 Now the color recognition is better 2023-04-08 08:45:57 +02:00
Maxluli c515e5ff62 Tyre recognition is still fucked but im going to bed rn 2023-04-07 17:05:50 +02:00
Maxluli 124d141181 Trying to implement tyres but not working for now 2023-04-07 15:58:12 +02:00
17 changed files with 937 additions and 352 deletions
+102
View File
@@ -0,0 +1,102 @@
/// Author : Maxime Rohmer
/// Date : 25/04/2023
/// File : DriverData.cs
/// Brief : File that contains strcutures to store the found data
/// Version : 0.1
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OCR_Decode
{
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 += "Gap to leader : " + Reader.ConvertMsToTime(GapToLeader) + Environment.NewLine;
//LapTime
result += "Lap time : " + 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 += "Driver name : " + Name +Environment.NewLine;
//Sector 1
result += "Sector 1 : " + Reader.ConvertMsToTime(Sector1) + Environment.NewLine;
//Sector 1
result += "Sector 2 : " + Reader.ConvertMsToTime(Sector2) + Environment.NewLine;
//Sector 1
result += "Sector 3 : " + 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;
}
}
}
+2 -3
View File
@@ -18,7 +18,7 @@ namespace OCR_Decode
{ {
Name = "DRS"; Name = "DRS";
} }
public override object DecodePng() public override async Task<object> DecodePng()
{ {
bool result = false; bool result = false;
int greenValue = GetGreenPixels(); int greenValue = GetGreenPixels();
@@ -28,7 +28,7 @@ namespace OCR_Decode
if (greenValue > EmptyDrsGreenValue + EmptyDrsGreenValue / 100 * 30) if (greenValue > EmptyDrsGreenValue + EmptyDrsGreenValue / 100 * 30)
result = true; result = true;
WindowImage.Save(Reader.DEBUG_DUMP_FOLDER + "DRS" + result + ".png"); //WindowImage.Save(Reader.DEBUG_DUMP_FOLDER + "DRS" + result + ".png");
return result; return result;
} }
@@ -68,7 +68,6 @@ namespace OCR_Decode
} }
public Rectangle GetBox() public Rectangle GetBox()
{ {
var tessImage = Pix.LoadFromMemory(ImageToByte(WindowImage)); var tessImage = Pix.LoadFromMemory(ImageToByte(WindowImage));
Engine.SetVariable("tessedit_char_whitelist", ""); Engine.SetVariable("tessedit_char_whitelist", "");
Page page = Engine.Process(tessImage); Page page = Engine.Process(tessImage);
+14 -3
View File
@@ -1,9 +1,16 @@
using System; /// Author : Maxime Rohmer
/// Date : 25/04/2023
/// File : DriverGapToLeaderWindow.cs
/// Brief : Window that contains infos about the driver Gap to leader
/// Version : 0.1
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Tesseract;
namespace OCR_Decode namespace OCR_Decode
{ {
@@ -13,9 +20,13 @@ namespace OCR_Decode
{ {
Name = "Gap to leader"; Name = "Gap to leader";
} }
public override object DecodePng() /// <summary>
/// Decodes the gap to leader using Tesseract OCR
/// </summary>
/// <returns></returns>
public override async Task<object> DecodePng()
{ {
int result = GetTimeFromPng(WindowImage, OcrImage.WindowType.Gap); int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Gap, Engine);
return result; return result;
} }
} }
+13 -3
View File
@@ -1,4 +1,10 @@
using System; /// Author : Maxime Rohmer
/// Date : 25/04/2023
/// File : DriverLapTimeWindow.cs
/// Brief : Window that contains infos about a laptime of the driver
/// Version : 0.1
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
@@ -14,9 +20,13 @@ namespace OCR_Decode
{ {
Name = "Lap time"; Name = "Lap time";
} }
public override object DecodePng() /// <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 = GetTimeFromPng(WindowImage,OcrImage.WindowType.LapTime); int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.LapTime,Engine);
return result; return result;
} }
} }
+22 -5
View File
@@ -1,4 +1,10 @@
using System; /// Author : Maxime Rohmer
/// Date : 25/04/2023
/// File : DriverNameWindow.cs
/// Brief : Window that contains infos about the driver name in it
/// Version : 0.1
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
@@ -15,10 +21,15 @@ namespace OCR_Decode
{ {
Name = "Name"; Name = "Name";
} }
public override object DecodePng(List<string> DriverList) /// <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 = ""; string result = "";
result = GetStringFromPng(); result = await GetStringFromPng(WindowImage,Engine);
if (!IsADriver(DriverList, result)) if (!IsADriver(DriverList, result))
{ {
@@ -27,11 +38,17 @@ namespace OCR_Decode
} }
return result; return result;
} }
private static bool IsADriver(List<string> drivers, string potentialDriver) /// <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; bool result = false;
//I cant use drivers.Contains because it has missmatched cases and all //I cant use drivers.Contains because it has missmatched cases and all
foreach (string name in drivers) foreach (string name in driverList)
{ {
if (name.ToUpper() == potentialDriver.ToUpper()) if (name.ToUpper() == potentialDriver.ToUpper())
result = true; result = true;
+16 -6
View File
@@ -1,9 +1,16 @@
using System; /// Author : Maxime Rohmer
/// Date : 25/04/2023
/// File : DriverPositionWindow.cs
/// Brief : Window that contains infos about a driver position
/// Version : 0.1
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Tesseract;
namespace OCR_Decode namespace OCR_Decode
{ {
@@ -13,19 +20,22 @@ namespace OCR_Decode
{ {
Name = "Position"; Name = "Position";
} }
public override object DecodePng() /// <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 result = GetStringFromPng(true); string ocrResult = await GetStringFromPng(WindowImage,Engine, "0123456789");
int position; int position;
try try
{ {
position = Convert.ToInt32(result); position = Convert.ToInt32(ocrResult);
} }
catch catch
{ {
position = -1; position = -1;
WindowImage.Save(Reader.DEBUG_DUMP_FOLDER + "FailedPosition"+".png");
} }
return position; return position;
} }
+15 -4
View File
@@ -1,9 +1,16 @@
using System; /// Author : Maxime Rohmer
/// Date : 25/04/2023
/// File : DriverSector1Window.cs
/// Brief : Window that contains infos about the first sector of a driver
/// Version : 0.1
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Tesseract;
namespace OCR_Decode namespace OCR_Decode
{ {
@@ -13,10 +20,14 @@ namespace OCR_Decode
{ {
Name = "Sector 1"; Name = "Sector 1";
} }
public override object DecodePng() /// <summary>
/// Decodes the sector
/// </summary>
/// <returns>the sector time in int (ms)</returns>
public override async Task<object> DecodePng()
{ {
int result = GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector); int ocrResult = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector, Engine);
return result; return ocrResult;
} }
} }
} }
+16 -4
View File
@@ -1,9 +1,17 @@
using System; /// Author : Maxime Rohmer
/// Date : 25/04/2023
/// File : DriverSector2Window.cs
/// Brief : Window that contains infos about the second sector of a driver
/// Version : 0.1
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Tesseract;
namespace OCR_Decode namespace OCR_Decode
{ {
@@ -13,10 +21,14 @@ namespace OCR_Decode
{ {
Name = "Sector 2"; Name = "Sector 2";
} }
public override object DecodePng() /// <summary>
/// Decodes the sector
/// </summary>
/// <returns>the sector time in int (ms)</returns>
public override async Task<object> DecodePng()
{ {
int result = GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector); int ocrResult = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector,Engine);
return result; return ocrResult;
} }
} }
} }
+16 -4
View File
@@ -1,9 +1,17 @@
using System; /// Author : Maxime Rohmer
/// Date : 25/04/2023
/// File : DriverSector3Window.cs
/// Brief : Window that contains infos about the third sector of a driver
/// Version : 0.1
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Tesseract;
namespace OCR_Decode namespace OCR_Decode
{ {
@@ -13,10 +21,14 @@ namespace OCR_Decode
{ {
Name = "Sector 3"; Name = "Sector 3";
} }
public override object DecodePng() /// <summary>
/// Decodes the sector
/// </summary>
/// <returns>the sector time in int (ms)</returns>
public override async Task<object> DecodePng()
{ {
int result = GetTimeFromPng(WindowImage,OcrImage.WindowType.Sector); int ocrResult = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector,Engine);
return result; return ocrResult;
} }
} }
} }
+103 -88
View File
@@ -1,5 +1,12 @@
using System; /// Author : Maxime Rohmer
/// Date : 25/04/2023
/// File : DriverTyresWindow.cs
/// Brief : Window that contains infos about tyres and that can decode them
/// Version : 0.1
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -11,118 +18,126 @@ namespace OCR_Decode
internal class DriverTyresWindow : Window internal class DriverTyresWindow : Window
{ {
private static Random rnd = new Random(); 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) : base(image, bounds) public DriverTyresWindow(Bitmap image, Rectangle bounds) : base(image, bounds)
{ {
Name = "Tyres"; Name = "Tyres";
} }
public override object DecodePng() /// <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()
{ {
//WindowImage.Save(Reader.DEBUG_DUMP_FOLDER + "testTyres" + rnd.Next(0, 1000) + ".png"); return await GetTyreInfos();
return GetTyreInfos();
} }
private Tyre 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());
string result = ""; Tyre.Type type = Tyre.Type.Undefined;
type = GetTyreTypeFromColor(OcrImage.GetAvgColorFromBitmap(tyreZone));
Bitmap rawData = WindowImage; int laps = -1;
//Hards softs mediums Inter Wet -> HSMIW
Engine.SetVariable("tessedit_char_whitelist", "0123456789HSMIW"); string number = await GetStringFromPng(tyreZone, Engine, "0123456789", OcrImage.WindowType.Tyre);
Page page = Engine.Process(rawData); try
Rectangle TyreRadius = new Rectangle(0, 0, 0, 0);
using (var iter = page.GetIterator())
{ {
iter.Begin(); laps = Convert.ToInt32(number);
do
{
Rect boundingBox;
// Get the bounding box for the current element
if (iter.TryGetBoundingBox(PageIteratorLevel.Word, out boundingBox))
{
int xOffset = ((boundingBox.Width / 100) * 20) / 2;
int yOffset = ((boundingBox.Height / 100) * 20) / 2;
using (Graphics g = Graphics.FromImage(WindowImage))
{
//TyreRadius = new Rectangle(boundingBox.X1 - xOffset, boundingBox.Y1 - yOffset, boundingBox.Width + xOffset * 2, boundingBox.Height + yOffset * 2);
TyreRadius = new Rectangle(boundingBox.X1,boundingBox.Y1,boundingBox.Width,boundingBox.Height);
g.DrawRectangle(Pens.AliceBlue,TyreRadius);
GetSmallBitmapFromBigOne(WindowImage,TyreRadius).Save(Reader.DEBUG_DUMP_FOLDER + "Tyres" + result + ".png");
}
}
result += iter.GetText(PageIteratorLevel.Word);
} while (iter.Next(PageIteratorLevel.Word));
} }
catch
//rawData.Save(Reader.DEBUG_DUMP_FOLDER + "Tyres" + result + ".png"); {
//We could not convert the number so its a letter so its 0 laps old
page.Dispose(); laps = 0;
*/ }
//tyreZone.Save(Reader.DEBUG_DUMP_FOLDER + "Tyre" + type + "Laps" + laps + '#' + rnd.Next(0, 1000) + ".png");
GetSmallBitmapFromBigOne(WindowImage,FindTyreZone()).Save(Reader.DEBUG_DUMP_FOLDER + "Tyres" + rnd.Next(0,1000) + ".png"); return new Tyre(type, laps);
return new Tyre(Tyre.Type.Undefined,0);
} }
/// <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() private Rectangle FindTyreZone()
{ {
Bitmap bmp = WindowImage; Bitmap bmp = WindowImage;
int currentPosition = bmp.Width; int currentPosition = bmp.Width;
int height = bmp.Height / 2; int height = bmp.Height / 2;
Color limitColor = Color.FromArgb(0x50,0x50,0x50); Color limitColor = Color.FromArgb(0x50, 0x50, 0x50);
Color currentColor = Color.FromArgb(0,0,0); Color currentColor = Color.FromArgb(0, 0, 0);
Size newWindowSize = new Size(bmp.Height,bmp.Height); 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) while (currentColor.R <= limitColor.R && currentColor.G <= limitColor.G && currentColor.B <= limitColor.B && currentPosition > 0)
{ {
currentPosition--; currentPosition--;
currentColor = bmp.GetPixel(currentPosition,height); currentColor = bmp.GetPixel(currentPosition, height);
} }
//Its here to let the new window include a little bit of the right side //Its here to let the new window include a little bit of the right
int offset = Convert.ToInt32((float)newWindowSize.Width / 100f * 20f); int CorrectedX = currentPosition - (newWindowSize.Width) + Convert.ToInt32((float)newWindowSize.Width / 100f * 10f);
int CorrectedX = currentPosition - (newWindowSize.Width - offset); int CorrectedY = Convert.ToInt32((float)newWindowSize.Height / 100f * 35f);
if (CorrectedX <= 0) if (CorrectedX <= 0)
return new Rectangle(0,0,newWindowSize.Width,newWindowSize.Height); return new Rectangle(0, 0, newWindowSize.Width, newWindowSize.Height);
return new Rectangle(CorrectedX,0,newWindowSize.Width,newWindowSize.Height); return new Rectangle(CorrectedX, CorrectedY, newWindowSize.Width, newWindowSize.Height);
} }
private Bitmap GetSmallBitmapFromBigOne(Bitmap bmp, Rectangle rectangle) //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)
{ {
Bitmap sample = new Bitmap(rectangle.Width, rectangle.Height); Tyre.Type type = Tyre.Type.Undefined;
Graphics g = Graphics.FromImage(sample); List<Color> colors = new List<Color>();
g.DrawImage(bmp, new Rectangle(0, 0, sample.Width, sample.Height), rectangle, GraphicsUnit.Pixel); //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
return sample; //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);
public Tyre.Type GetColor(Bitmap bmp) colors.Add(HARD_TYRE_COLOR);
{ colors.Add(INTER_TYRE_COLOR);
return Color.Violet; colors.Add(WET_TYRE_COLOR);
} colors.Add(EMPTY_COLOR);
*/
} Color closestColor = colors[0];
struct Tyre int closestDistance = int.MaxValue;
{ foreach (Color color in colors)
public enum Type {
{ int distance = Math.Abs(color.R - inputColor.R) + Math.Abs(color.G - inputColor.G) + Math.Abs(color.B - inputColor.B);
Soft, if (distance < closestDistance)
Medium, {
Hard, closestColor = color;
Inter, closestDistance = distance;
Wet, }
Undefined }
}
public Type Coumpound; //We cant use a switch as the colors cant be constants ...
public int NumberOfLaps; if (closestColor == SOFT_TYRE_COLOR)
public Tyre(Type type, int laps) type = Tyre.Type.Soft;
{ if (closestColor == MEDIUM_TYRE_COLOR)
Coumpound = type; type = Tyre.Type.Medium;
NumberOfLaps = laps; 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;
} }
} }
} }
+24 -3
View File
@@ -7,6 +7,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using System.Diagnostics;
namespace OCR_Decode namespace OCR_Decode
{ {
@@ -17,6 +18,8 @@ namespace OCR_Decode
const string TEMPORARY_IMAGE_FOLDER = @"C:\Users\Moi\Pictures\SeleniumScreens\DataSetHighRes\"; const string TEMPORARY_IMAGE_FOLDER = @"C:\Users\Moi\Pictures\SeleniumScreens\DataSetHighRes\";
int pageNmbr = 82; int pageNmbr = 82;
Reader Reader; Reader Reader;
Stopwatch stopWatch = new Stopwatch();
int LoadMS = 0;
public Form1() public Form1()
{ {
InitializeComponent(); InitializeComponent();
@@ -24,16 +27,34 @@ namespace OCR_Decode
private void Form1_Load(object sender, EventArgs e) private void Form1_Load(object sender, EventArgs e)
{ {
stopWatch.Start();
Reader = new Reader(CONFIG_FILE,TEMPORARY_IMAGE_FOLDER); Reader = new Reader(CONFIG_FILE,TEMPORARY_IMAGE_FOLDER);
stopWatch.Stop();
LoadMS = (int)stopWatch.ElapsedMilliseconds;
RefreshUi(); RefreshUi();
} }
private void RefreshUi() private async void RefreshUi()
{ {
tbxResult.Text = ""; tbxResult.Text = "";
stopWatch.Restart();
pbxImage.Image = Reader.Draw(pageNmbr); pbxImage.Image = Reader.Draw(pageNmbr);
stopWatch.Stop();
tbxResult.Text = Reader.Decode(pageNmbr); int DrawMS = (int)stopWatch.ElapsedMilliseconds;
stopWatch.Restart();
tbxResult.Text = await Reader.Decode(pageNmbr);
stopWatch.Stop();
int OcrMS = (int)stopWatch.ElapsedMilliseconds;
MessageBox.Show(
"Loading took : " + Reader.ConvertMsToTime(LoadMS) +
" Image splitting took : " + Reader.ConvertMsToTime(DrawMS) + Environment.NewLine +
" Reading the infos from the image took : " + Reader.ConvertMsToTime(OcrMS) + Environment.NewLine
);
GC.Collect();
} }
private void btnNext_Click(object sender, EventArgs e) private void btnNext_Click(object sender, EventArgs e)
+1
View File
@@ -107,6 +107,7 @@
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="DriverData.cs" />
<Compile Include="DriverDrsWindow.cs" /> <Compile Include="DriverDrsWindow.cs" />
<Compile Include="DriverGapToLeaderWindow.cs" /> <Compile Include="DriverGapToLeaderWindow.cs" />
<Compile Include="DriverLapTimeWindow.cs" /> <Compile Include="DriverLapTimeWindow.cs" />
+367 -93
View File
@@ -1,4 +1,10 @@
using System; /// Author : Maxime Rohmer
/// Date : 25/04/2023
/// File : OcrImage.cs
/// Brief : Class that contains all the post processing methods that can help with OCR
/// Version : 0.1
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -14,63 +20,128 @@ namespace OCR_Decode
//Just so you know //Just so you know
public class OcrImage public class OcrImage
{ {
Bitmap InputImage; //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 public enum WindowType
{ {
LapTime, LapTime,
Text, Text,
Sector, Sector,
Gap, Gap,
Tyre,
} }
public OcrImage(Bitmap inputImage) //For debugging purposes
public bool DumpDebugImages;
Random rnd = new Random();
int salt = 0;
/// <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)
{ {
InputImage = inputImage; InputBitmap = inputBitmap;
//Before turning this on check that the DEBUG_DUMP_FOLDER constant has been changed in the Reader.cs file
DumpDebugImages = false;
//To help debugging
salt = rnd.Next(0, 10000);
} }
/// <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) public Bitmap Enhance(WindowType type = WindowType.Text)
{ {
Bitmap result = (Bitmap)InputImage.Clone(); Bitmap outputBitmap = (Bitmap)InputBitmap.Clone();
switch (type) switch (type)
{ {
case WindowType.LapTime: case WindowType.LapTime:
result = Grayscale(result); if (DumpDebugImages)
result = InvertColors(result); outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "LapTimeBefore_" + salt + ".png");
result = Tresholding(result, 165);
result = Resize(result); outputBitmap = Tresholding(outputBitmap, 185);
result = Resize(result); if (DumpDebugImages)
outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "LapTimeFilter1_" + salt + ".png");
outputBitmap = Resize(outputBitmap, 2);
if (DumpDebugImages)
outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "LapTimeFilter2_" + salt + ".png");
outputBitmap = Dilatation(outputBitmap, 1);
if (DumpDebugImages)
outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "LapTimeFilter3_" + salt + ".png");
outputBitmap = Erode(outputBitmap, 1);
if (DumpDebugImages)
outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "LapTimeAfter_" + salt + ".png");
break; break;
case WindowType.Text: case WindowType.Text:
result = Grayscale(result); if (DumpDebugImages)
result = InvertColors(result); outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "TextBefore_" + salt + ".png");
result = Tresholding(result, 165);
result = Resize(result); outputBitmap = InvertColors(outputBitmap);
result = Dilatation(result, 1); if (DumpDebugImages)
outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "TextFilter1_" + salt + ".png");
outputBitmap = Tresholding(outputBitmap, 165);
if (DumpDebugImages)
outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "TextFilter2_" + salt + ".png");
outputBitmap = Resize(outputBitmap, 2);
if (DumpDebugImages)
outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "TextFilter3_" + salt + ".png");
outputBitmap = Dilatation(outputBitmap, 1);
if (DumpDebugImages)
outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "TextAfter_" + salt + ".png");
break;
case WindowType.Tyre:
if (DumpDebugImages)
outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "TyreBefore_" + salt + ".png");
outputBitmap = RemoveUseless(outputBitmap);
if (DumpDebugImages)
outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "TyreFilter01_" + salt + ".png");
outputBitmap = Resize(outputBitmap, 4);
if (DumpDebugImages)
outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "TyreFilter02_" + salt + ".png");
outputBitmap = Dilatation(outputBitmap, 1);
if (DumpDebugImages)
outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "TyreAfter_" + salt + ".png");
break; break;
default: default:
result = Tresholding(result, 165); outputBitmap = Tresholding(outputBitmap, 165);
result = Resize(result); outputBitmap = Resize(outputBitmap, 4);
result = Resize(result); outputBitmap = Erode(outputBitmap, 1);
result = Erode(result, 1);
break; break;
} }
return outputBitmap;
//result = Dilatation(result, 1);
return result;
} }
public static Bitmap Grayscale(Bitmap input) /// <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)
{ {
Bitmap bmp = input; Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
unsafe unsafe
{ {
byte* ptr = (byte*)bmpData.Scan0.ToPointer(); byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < bmp.Height; y++) for (int y = 0; y < inputBitmap.Height; y++)
{ {
byte* currentLine = ptr + (y * bmpData.Stride); byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < bmp.Width; x++) for (int x = 0; x < inputBitmap.Width; x++)
{ {
byte* pixel = currentLine + (x * bytesPerPixel); byte* pixel = currentLine + (x * bytesPerPixel);
@@ -78,62 +149,254 @@ namespace OCR_Decode
byte green = pixel[1]; byte green = pixel[1];
byte red = pixel[2]; 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 gray = (int)(red * 0.3 + green * 0.59 + blue * 0.11);
pixel[0] = pixel[1] = pixel[2] = (byte)gray; pixel[0] = pixel[1] = pixel[2] = (byte)gray;
} }
} }
} }
bmp.UnlockBits(bmpData); inputBitmap.UnlockBits(bmpData);
return bmp; return inputBitmap;
} }
public static Bitmap Tresholding(Bitmap input, int threshold) /// <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)
{ {
Bitmap bmp = input; Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
unsafe unsafe
{ {
byte* ptr = (byte*)bmpData.Scan0.ToPointer(); byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < bmp.Height; y++) int bmpHeight = inputBitmap.Height;
int bmpWidth = inputBitmap.Width;
Parallel.For(0, bmpHeight, y =>
{ {
byte* currentLine = ptr + (y * bmpData.Stride); byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < bmp.Width; x++) for (int x = 0; x < bmpWidth; x++)
{ {
byte* pixel = currentLine + (x * bytesPerPixel); byte* pixel = currentLine + (x * bytesPerPixel);
byte blue = pixel[0]; byte blue = pixel[0];
byte green = pixel[1]; byte green = pixel[1];
byte red = pixel[2]; 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 gray = (int)(red * 0.3 + green * 0.59 + blue * 0.11);
int value = gray < threshold ? 0 : 255; int value = gray < threshold ? 0 : 255;
pixel[0] = pixel[1] = pixel[2] = (byte)value; pixel[0] = pixel[1] = pixel[2] = (byte)value;
} }
} });
} }
bmp.UnlockBits(bmpData); inputBitmap.UnlockBits(bmpData);
return bmp; return inputBitmap;
} }
public static Bitmap InvertColors(Bitmap input) /// <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)
{ {
Bitmap bmp = input; Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height);
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8;
int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
unsafe unsafe
{ {
byte* ptr = (byte*)bmpData.Scan0.ToPointer(); byte* ptr = (byte*)bmpData.Scan0.ToPointer();
for (int y = 0; y < bmp.Height; y++) for (int y = 0; y < inputBitmap.Height; y++)
{ {
byte* currentLine = ptr + (y * bmpData.Stride); byte* currentLine = ptr + (y * bmpData.Stride);
for (int x = 0; x < bmp.Width; x++) 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); byte* pixel = currentLine + (x * bytesPerPixel);
@@ -143,47 +406,47 @@ namespace OCR_Decode
} }
} }
} }
bmp.UnlockBits(bmpData); inputBitmap.UnlockBits(bmpData);
return bmp; return inputBitmap;
} }
public static Bitmap ApplyGaussianBlur(Bitmap input, int radius) /// <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)
{ {
GaussianBlur filter = new GaussianBlur(radius); var resultBitmap = new Bitmap(inputBitmap.Width * resizeFactor, inputBitmap.Height * resizeFactor);
Bitmap output = filter.Apply(input);
return output;
}
public static Bitmap Resize(Bitmap sourceBitmap)
{
// Create a new Bitmap object to hold the resized image
var resultBitmap = new Bitmap(sourceBitmap.Width * 2, sourceBitmap.Height * 2);
// Create a Graphics object to draw the resized image
using (var graphics = Graphics.FromImage(resultBitmap)) using (var graphics = Graphics.FromImage(resultBitmap))
{ {
// Set the interpolation mode to high-quality bicubic
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.DrawImage(inputBitmap, new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height));
// Draw the resized image using the Graphics object
graphics.DrawImage(sourceBitmap, new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height));
} }
return resultBitmap; return resultBitmap;
} }
public static Bitmap HighlightContours(Bitmap input) /// <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 output = new Bitmap(input.Width, input.Height); Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height);
Bitmap grayscale = Grayscale(input); Bitmap grayscale = Grayscale(inputBitmap);
Bitmap thresholded = Tresholding(grayscale, 128); Bitmap thresholded = Tresholding(grayscale, 128);
Bitmap dilated = Dilatation(thresholded, 3); Bitmap dilated = Dilatation(thresholded, 3);
Bitmap eroded = Erode(dilated, 3); Bitmap eroded = Erode(dilated, 3);
for (int y = 0; y < input.Height; y++) for (int y = 0; y < inputBitmap.Height; y++)
{ {
for (int x = 0; x < input.Width; x++) for (int x = 0; x < inputBitmap.Width; x++)
{ {
Color pixel = input.GetPixel(x, y); Color pixel = inputBitmap.GetPixel(x, y);
Color dilatedPixel = dilated.GetPixel(x, y); Color dilatedPixel = dilated.GetPixel(x, y);
Color erodedPixel = eroded.GetPixel(x, y); Color erodedPixel = eroded.GetPixel(x, y);
@@ -192,24 +455,30 @@ namespace OCR_Decode
if (gray > threshold) if (gray > threshold)
{ {
output.SetPixel(x, y, Color.FromArgb(255, 255, 255)); outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255));
} }
else if (gray <= threshold && erodedPixel.R == 0) else if (gray <= threshold && erodedPixel.R == 0)
{ {
output.SetPixel(x, y, Color.FromArgb(255, 0, 0)); outputBitmap.SetPixel(x, y, Color.FromArgb(255, 0, 0));
} }
else else
{ {
output.SetPixel(x, y, Color.FromArgb(0, 0, 0)); outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0));
} }
} }
} }
return output; return outputBitmap;
} }
public static Bitmap Erode(Bitmap input, int kernelSize) /// <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 output = new Bitmap(input.Width, input.Height); Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height);
int[,] kernel = new int[kernelSize, kernelSize]; int[,] kernel = new int[kernelSize, kernelSize];
@@ -221,9 +490,9 @@ namespace OCR_Decode
} }
} }
for (int y = kernelSize / 2; y < input.Height - kernelSize / 2; y++) for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++)
{ {
for (int x = kernelSize / 2; x < input.Width - kernelSize / 2; x++) for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++)
{ {
bool flag = true; bool flag = true;
@@ -231,7 +500,7 @@ namespace OCR_Decode
{ {
for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) for (int j = -kernelSize / 2; j <= kernelSize / 2; j++)
{ {
Color pixel = input.GetPixel(x + i, y + j); Color pixel = inputBitmap.GetPixel(x + i, y + j);
int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); 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) if (gray >= 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1)
@@ -249,21 +518,26 @@ namespace OCR_Decode
if (flag) if (flag)
{ {
output.SetPixel(x, y, Color.FromArgb(255, 255, 255)); outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255));
} }
else else
{ {
output.SetPixel(x, y, Color.FromArgb(0, 0, 0)); outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0));
} }
} }
} }
return output; return outputBitmap;
} }
/// <summary>
public static Bitmap Dilatation(Bitmap input, int kernelSize) /// 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 output = new Bitmap(input.Width, input.Height); Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height);
int[,] kernel = new int[kernelSize, kernelSize]; int[,] kernel = new int[kernelSize, kernelSize];
@@ -275,9 +549,9 @@ namespace OCR_Decode
} }
} }
for (int y = kernelSize / 2; y < input.Height - kernelSize / 2; y++) for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++)
{ {
for (int x = kernelSize / 2; x < input.Width - kernelSize / 2; x++) for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++)
{ {
bool flag = false; bool flag = false;
@@ -285,7 +559,7 @@ namespace OCR_Decode
{ {
for (int j = -kernelSize / 2; j <= kernelSize / 2; j++) for (int j = -kernelSize / 2; j <= kernelSize / 2; j++)
{ {
Color pixel = input.GetPixel(x + i, y + j); Color pixel = inputBitmap.GetPixel(x + i, y + j);
int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); 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) if (gray < 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1)
@@ -303,16 +577,16 @@ namespace OCR_Decode
if (flag) if (flag)
{ {
output.SetPixel(x, y, Color.FromArgb(0, 0, 0)); outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0));
} }
else else
{ {
output.SetPixel(x, y, Color.FromArgb(255, 255, 255)); outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255));
} }
} }
} }
return output; return outputBitmap;
} }
} }
+67 -76
View File
@@ -1,4 +1,10 @@
using System; /// Author : Maxime Rohmer
/// Date : 25/04/2023
/// File : Reader.cs
/// Brief : Model that contains all the classes and methods to manage the OCR
/// Version : 0.1
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -7,6 +13,7 @@ using System.Drawing;
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using System.Windows.Forms; using System.Windows.Forms;
using System.Diagnostics;
namespace OCR_Decode namespace OCR_Decode
{ {
@@ -22,7 +29,9 @@ namespace OCR_Decode
public List<string> Drivers { get => _drivers; private set => _drivers = value; } public List<string> Drivers { get => _drivers; private set => _drivers = value; }
public List<Zone> MainZones { get => _mainZones; set => _mainZones = value; } public List<Zone> MainZones { get => _mainZones; set => _mainZones = value; }
//All the image infos will be deleted in not too much time when the merge with the program that recovers the images
const string DEFAULT_IMAGE_NAME = "screen_"; const string DEFAULT_IMAGE_NAME = "screen_";
// You will defenitely have to change this if you want to be able to see debug images
public const string DEBUG_DUMP_FOLDER = @"C:\Users\Moi\Desktop\imgDump\Decode\"; public const string DEBUG_DUMP_FOLDER = @"C:\Users\Moi\Desktop\imgDump\Decode\";
const int NUMBER_OF_DRIVERS = 20; const int NUMBER_OF_DRIVERS = 20;
@@ -32,6 +41,10 @@ namespace OCR_Decode
ImagesFolder = imageFolder; ImagesFolder = imageFolder;
Load(82); Load(82);
} }
/// <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>
private void Load(int imageNumber) private void Load(int imageNumber)
{ {
MainZones = new List<Zone>(); MainZones = new List<Zone>();
@@ -115,25 +128,36 @@ namespace OCR_Decode
Point driverSector3Position = new Point(driverSector3.GetProperty("x").GetInt32(), driverSector3.GetProperty("y").GetInt32()); Point driverSector3Position = new Point(driverSector3.GetProperty("x").GetInt32(), driverSector3.GetProperty("y").GetInt32());
float offset = (((float)MainZone.ZoneImage.Height - (float)(Drivers.Count * FirstZoneSize.Height)) / (float)Drivers.Count); float offset = (((float)MainZone.ZoneImage.Height - (float)(Drivers.Count * FirstZoneSize.Height)) / (float)Drivers.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++) for (int i = 0; i < NUMBER_OF_DRIVERS; i++)
{ {
Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset) /*- (i* (FirstZoneSize.Height / 32))*/); Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset));
Zone newDriverZone = new Zone(MainZone.ZoneImage, new Rectangle(tmpPos, FirstZoneSize)); Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize));
zonesToAdd.Add(newDriverZone);
newDriverZone.AddWindow(new DriverPositionWindow(newDriverZone.ZoneImage, new Rectangle(driverPositionPosition, driverPositionArea))); zonesImages.Add(newDriverZone.ZoneImage);
newDriverZone.AddWindow(new DriverGapToLeaderWindow(newDriverZone.ZoneImage, new Rectangle(driverGapToLeaderPosition, driverGapToLeaderArea)));
newDriverZone.AddWindow(new DriverLapTimeWindow(newDriverZone.ZoneImage, new Rectangle(driverLapTimePosition, driverLapTimeArea)));
newDriverZone.AddWindow(new DriverDrsWindow(newDriverZone.ZoneImage, new Rectangle(driverDrsPosition, driverDrsArea)));
newDriverZone.AddWindow(new DriverTyresWindow(newDriverZone.ZoneImage, new Rectangle(driverTyresPosition, driverTyresArea)));
newDriverZone.AddWindow(new DriverNameWindow(newDriverZone.ZoneImage, new Rectangle(driverNamePosition, driverNameArea)));
newDriverZone.AddWindow(new DriverSector1Window(newDriverZone.ZoneImage, new Rectangle(driverSector1Position, driverSector1Area)));
newDriverZone.AddWindow(new DriverSector2Window(newDriverZone.ZoneImage, new Rectangle(driverSector2Position, driverSector2Area)));
newDriverZone.AddWindow(new DriverSector3Window(newDriverZone.ZoneImage, new Rectangle(driverSector3Position, driverSector3Area)));
MainZone.AddZone(newDriverZone);
} }
//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)));
newDriverZone.AddWindow(new DriverGapToLeaderWindow(zoneImg, new Rectangle(driverGapToLeaderPosition, driverGapToLeaderArea)));
newDriverZone.AddWindow(new DriverLapTimeWindow(zoneImg, new Rectangle(driverLapTimePosition, driverLapTimeArea)));
newDriverZone.AddWindow(new DriverDrsWindow(zoneImg, new Rectangle(driverDrsPosition, driverDrsArea)));
newDriverZone.AddWindow(new DriverTyresWindow(zoneImg, new Rectangle(driverTyresPosition, driverTyresArea)));
newDriverZone.AddWindow(new DriverNameWindow(zoneImg, new Rectangle(driverNamePosition, driverNameArea)));
newDriverZone.AddWindow(new DriverSector1Window(zoneImg, new Rectangle(driverSector1Position, driverSector1Area)));
newDriverZone.AddWindow(new DriverSector2Window(zoneImg, new Rectangle(driverSector2Position, driverSector2Area)));
newDriverZone.AddWindow(new DriverSector3Window(zoneImg, new Rectangle(driverSector3Position, driverSector3Area)));
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"); //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); MainZones.Add(MainZone);
} }
@@ -147,6 +171,10 @@ namespace OCR_Decode
MessageBox.Show("Invalid JSON format: " + ex.Message); MessageBox.Show("Invalid JSON format: " + ex.Message);
} }
} }
/// <summary>
/// Changes the zones and windows to the new image
/// </summary>
/// <param name="imageNumber">The #id of the new image on wich to do OCR</param>
public void ChangeImage(int imageNumber) public void ChangeImage(int imageNumber)
{ {
Bitmap img = null; Bitmap img = null;
@@ -168,14 +196,19 @@ namespace OCR_Decode
} }
} }
} }
public string Decode(int idImage) /// <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(int idImage)
{ {
string result = ""; string result = "";
ChangeImage(idImage); ChangeImage(idImage);
List<List<Object>> mainResults = new List<List<Object>>(); List<DriverData> mainResults = new List<DriverData>();
//Decode //Decode
for (int mainZoneId = 0; mainZoneId < MainZones.Count;mainZoneId ++) for (int mainZoneId = 0; mainZoneId < MainZones.Count; mainZoneId++)
{ {
switch (mainZoneId) switch (mainZoneId)
{ {
@@ -183,77 +216,41 @@ namespace OCR_Decode
//Main Zone //Main Zone
foreach (Zone z in MainZones[mainZoneId].Zones) foreach (Zone z in MainZones[mainZoneId].Zones)
{ {
mainResults.Add(z.Decode(Drivers)); mainResults.Add(await z.Decode(Drivers));
} }
break; break;
//Next there will be the title and laps added //Next there could be a Title Zone and TrackInfoZone
} }
} }
//Display //Display
foreach (List<Object> objects in mainResults) foreach (DriverData driver in mainResults)
{ {
for (int objId = 0; objId < objects.Count; objId++) result += driver.ToString();
{ result += Environment.NewLine;
switch (objId)
{
case 0:
//Position
result += "Position : " + (int)objects[objId] + " ";
break;
case 1:
//Gap
result += "Gap to leader : " + ConvertMsToTime((int)objects[objId]) + " ";
break;
case 2:
//LapTime
result += "Lap time : " + ConvertMsToTime((int)objects[objId]) + " ";
break;
case 3:
//DRS
result += "DRS : " + (bool)objects[objId] + "";
break;
case 4:
//Tyres
Tyre tyre = (Tyre)objects[objId];
result += "Tyre : " + tyre.Coumpound + " laps with the tyre : " + tyre.NumberOfLaps + " ";
break;
case 5:
//Name
result += "Driver name : " + (string)objects[objId] + " ";
break;
case 6:
//Sector 1
result += "Sector 1 : " + ConvertMsToTime((int)objects[objId]) + " ";
break;
case 7:
//Sector 1
result += "Sector 2 : " + ConvertMsToTime((int)objects[objId]) + " ";
break;
case 8:
//Sector 1
result += "Sector 3 : " + ConvertMsToTime((int)objects[objId]) + " ";
break;
default:
result += "Unknown source : " + objects[objId].ToString();
break;
}
result += Environment.NewLine;
}
} }
return result; 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) public static string ConvertMsToTime(int amountOfMs)
{ {
//Convert.ToInt32 would round upand I dont want that //Convert.ToInt32 would round upand I dont want that
int minuts = (int)((float)amountOfMs / (1000f * 60f)); int minuts = (int)((float)amountOfMs / (1000f * 60f));
int seconds = (int)((amountOfMs - (minuts*60f*1000f))/1000); int seconds = (int)((amountOfMs - (minuts * 60f * 1000f)) / 1000);
int ms = amountOfMs - ((minuts * 60 * 1000) + (seconds * 1000)); int ms = amountOfMs - ((minuts * 60 * 1000) + (seconds * 1000));
return minuts + ":" + seconds.ToString("00") + ":" + ms.ToString("000"); 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(int idImage) public Bitmap Draw(int idImage)
{ {
Bitmap result; Bitmap result;
@@ -274,12 +271,6 @@ namespace OCR_Decode
int count = 0; int count = 0;
foreach (Zone zz in z.Zones) foreach (Zone zz in z.Zones)
{ {
string driverFolder = DEBUG_DUMP_FOLDER + "driver" + count + "\\";
if (!Directory.Exists(driverFolder))
Directory.CreateDirectory(driverFolder);
zz.ZoneImage.Save(driverFolder + "FullImage.png");
g.DrawRectangle(Pens.Red, z.Bounds); g.DrawRectangle(Pens.Red, z.Bounds);
foreach (Window w in zz.Windows) foreach (Window w in zz.Windows)
{ {
+108 -47
View File
@@ -1,4 +1,10 @@
using System; /// Author : Maxime Rohmer
/// Date : 25/04/2023
/// File : Window.cs
/// Brief : Parent for futur windows and framework for the childrens
/// Version : 0.1
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -16,17 +22,18 @@ namespace OCR_Decode
private Rectangle _bounds; private Rectangle _bounds;
private Bitmap _image; private Bitmap _image;
private string _name; private string _name;
protected static TesseractEngine Engine; protected TesseractEngine Engine;
public Rectangle Bounds { get => _bounds; private set => _bounds = value; } public Rectangle Bounds { get => _bounds; private set => _bounds = value; }
public Bitmap Image { get => _image; set => _image = value; } public Bitmap Image { get => _image; set => _image = value; }
public string Name { get => _name; protected set => _name = 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 static DirectoryInfo TESS_DATA_FOLDER = new DirectoryInfo(@"C:\Users\Moi\Pictures\SeleniumScreens\TessData");
public Bitmap WindowImage public Bitmap WindowImage
{ {
get get
{ {
//This little trickery lets you have the image that the window sees
Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height);
Graphics g = Graphics.FromImage(sample); Graphics g = Graphics.FromImage(sample);
g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel);
@@ -37,40 +44,64 @@ namespace OCR_Decode
{ {
Image = image; Image = image;
Bounds = bounds; Bounds = bounds;
Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, "eng", EngineMode.Default); Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, "eng", EngineMode.Default);
Engine.DefaultPageSegMode = PageSegMode.SingleLine; Engine.DefaultPageSegMode = PageSegMode.SingleLine;
} }
public virtual Object DecodePng() /// <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"; return "NaN";
} }
public virtual Object DecodePng(List<string> drivers) /// <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"; return "NaN";
} }
public static byte[] ImageToByte(Image img) /// <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()) using (var stream = new MemoryStream())
{ {
img.Save(stream, System.Drawing.Imaging.ImageFormat.Png); inputImage.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
return stream.ToArray(); return stream.ToArray();
} }
} }
public static int GetTimeFromPng(Bitmap wImage,OcrImage.WindowType type) /// <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 = ""; string rawResult = "";
int result = 0; int result = 0;
switch (type) switch (windowType)
{ {
case OcrImage.WindowType.Sector: case OcrImage.WindowType.Sector:
//The usual sector is in this form : 33.456
Engine.SetVariable("tessedit_char_whitelist", "0123456789."); Engine.SetVariable("tessedit_char_whitelist", "0123456789.");
break; break;
case OcrImage.WindowType.LapTime: case OcrImage.WindowType.LapTime:
//The usual Lap time is in this form : 1:45:345
Engine.SetVariable("tessedit_char_whitelist", "0123456789.:"); Engine.SetVariable("tessedit_char_whitelist", "0123456789.:");
break; break;
case OcrImage.WindowType.Gap: case OcrImage.WindowType.Gap:
//The usual Gap is in this form : + 34.567
Engine.SetVariable("tessedit_char_whitelist", "0123456789.+"); Engine.SetVariable("tessedit_char_whitelist", "0123456789.+");
break; break;
default: default:
@@ -79,7 +110,7 @@ namespace OCR_Decode
} }
Bitmap enhancedImage = new OcrImage(wImage).Enhance(type); Bitmap enhancedImage = new OcrImage(windowImage).Enhance(windowType);
var tessImage = Pix.LoadFromMemory(ImageToByte(enhancedImage)); var tessImage = Pix.LoadFromMemory(ImageToByte(enhancedImage));
@@ -104,12 +135,10 @@ namespace OCR_Decode
} while (iter.Next(PageIteratorLevel.Word)); } while (iter.Next(PageIteratorLevel.Word));
} }
enhancedImage.Save(Reader.DEBUG_DUMP_FOLDER + Regex.Replace(rawResult, "[^0-9A-Za-z]", "") + ".png");
List<string> rawNumbers; List<string> rawNumbers;
//In the gaps we can find '+' but we dont care about it its redondant //In the gaps we can find '+' but we dont care about it its redondant a driver will never be - something
if(type == OcrImage.WindowType.Gap) if(windowType == OcrImage.WindowType.Gap)
rawResult = Regex.Replace(rawResult, "[^0-9.:]", ""); rawResult = Regex.Replace(rawResult, "[^0-9.:]", "");
//Splits into minuts seconds miliseconds //Splits into minuts seconds miliseconds
@@ -128,6 +157,17 @@ namespace OCR_Decode
{ {
//ss:ms //ss:ms
result = (Convert.ToInt32(rawNumbers[0]) * 1000) + Convert.ToInt32(rawNumbers[1]); 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 else
{ {
@@ -153,22 +193,22 @@ namespace OCR_Decode
page.Dispose(); page.Dispose();
return result; return result;
} }
protected string GetStringFromPng(bool onlyDigit = false) /// <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 = ""; string result = "";
if (onlyDigit) Engine.SetVariable("tessedit_char_whitelist", allowedChars);
{
Engine.SetVariable("tessedit_char_whitelist", "0123456789");
}
else
{
Engine.SetVariable("tessedit_char_whitelist", "");
}
Bitmap rawData = WindowImage; Bitmap rawData = WindowImage;
Bitmap enhancedImage = new OcrImage(rawData).Enhance(); Bitmap enhancedImage = new OcrImage(rawData).Enhance(windowType);
Page page = Engine.Process(enhancedImage); Page page = Engine.Process(enhancedImage);
using (var iter = page.GetIterator()) using (var iter = page.GetIterator())
@@ -178,22 +218,37 @@ namespace OCR_Decode
{ {
result += iter.GetText(PageIteratorLevel.Word); result += iter.GetText(PageIteratorLevel.Word);
} while (iter.Next(PageIteratorLevel.Word)); } while (iter.Next(PageIteratorLevel.Word));
} }
enhancedImage.Save(Reader.DEBUG_DUMP_FOLDER + Regex.Replace(result, "[^0-9A-Za-z]", "") + ".png");
page.Dispose(); page.Dispose();
return result; return result;
} }
//This method has been gnerated using ChatGPT /// <summary>
protected static string FindClosestMatch(List<string> array, string target) /// 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 closestMatch = "";
var closestDistance = int.MaxValue; var closestDistance = int.MaxValue;
foreach (var item in array) foreach (var item in options)
{ {
var distance = LevenshteinDistance(item, target); var distance = LevenshteinDistance(item, testString);
if (distance < closestDistance) if (distance < closestDistance)
{ {
closestMatch = item; closestMatch = item;
@@ -202,40 +257,46 @@ namespace OCR_Decode
} }
return closestMatch; return closestMatch;
} }
//This is a tool to be able to compare strings //This method has been generated with the help of ChatGPT
protected static int LevenshteinDistance(string s1, string s2) /// <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(s1)) if (string.IsNullOrEmpty(string1))
{ {
return string.IsNullOrEmpty(s2) ? 0 : s2.Length; return string.IsNullOrEmpty(string2) ? 0 : string2.Length;
} }
if (string.IsNullOrEmpty(s2)) if (string.IsNullOrEmpty(string2))
{ {
return string.IsNullOrEmpty(s1) ? 0 : s1.Length; return string.IsNullOrEmpty(string1) ? 0 : string1.Length;
} }
var d = new int[s1.Length + 1, s2.Length + 1]; var d = new int[string1.Length + 1, string2.Length + 1];
for (var i = 0; i <= s1.Length; i++) for (var i = 0; i <= string1.Length; i++)
{ {
d[i, 0] = i; d[i, 0] = i;
} }
for (var j = 0; j <= s2.Length; j++) for (var j = 0; j <= string2.Length; j++)
{ {
d[0, j] = j; d[0, j] = j;
} }
for (var i = 1; i <= s1.Length; i++) for (var i = 1; i <= string1.Length; i++)
{ {
for (var j = 1; j <= s2.Length; j++) for (var j = 1; j <= string2.Length; j++)
{ {
var cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; 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); 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[s1.Length, s2.Length]; return d[string1.Length, string2.Length];
} }
} }
} }
+44 -12
View File
@@ -1,4 +1,10 @@
using System; /// Author : Maxime Rohmer
/// Date : 25/04/2023
/// File : Zone.cs
/// Brief : Parent for futur zones and the that contains all the usefull methods for any zone
/// Version : 0.1
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -18,6 +24,7 @@ namespace OCR_Decode
{ {
get get
{ {
//This little trickery lets you have the image that the zone sees
Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height); Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height);
Graphics g = Graphics.FromImage(sample); Graphics g = Graphics.FromImage(sample);
g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel); g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel);
@@ -29,6 +36,7 @@ namespace OCR_Decode
get { return _image; } get { return _image; }
set set
{ {
//It automatically sets the image for the contained windows and zones
_image = Image; _image = Image;
foreach (Window w in Windows) foreach (Window w in Windows)
{ {
@@ -54,28 +62,52 @@ namespace OCR_Decode
_image = image; _image = image;
Bounds = bounds; 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) public virtual void AddZone(Zone zone)
{ {
Zones.Add(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) public virtual void AddWindow(Window window)
{ {
Windows.Add(window); Windows.Add(window);
} }
public virtual List<Object> Decode(List<string> drivers) /// <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)
{ {
List<Object> result = new List<Object>(); DriverData result = new DriverData();
foreach (Window w in Windows) Parallel.ForEach(Windows,async w =>
{ {
// A switch would be prettier but I dont think its supported in this C# version
if (w is DriverNameWindow) if (w is DriverNameWindow)
{ result.Name = (string)await (w as DriverNameWindow).DecodePng(driverList);
result.Add((w as DriverNameWindow).DecodePng(drivers)); if (w is DriverDrsWindow)
} result.DRS = (bool)await (w as DriverDrsWindow).DecodePng();
else if (w is DriverGapToLeaderWindow)
{ result.GapToLeader = (int)await (w as DriverGapToLeaderWindow).DecodePng();
result.Add(w.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 DriverSector1Window)
result.Sector1 = (int)await (w as DriverSector1Window).DecodePng();
if (w is DriverSector2Window)
result.Sector2 = (int)await (w as DriverSector2Window).DecodePng();
if (w is DriverSector3Window)
result.Sector3 = (int)await (w as DriverSector3Window).DecodePng();
if (w is DriverTyresWindow)
result.CurrentTyre = (Tyre)await (w as DriverTyresWindow).DecodePng();
});
return result; return result;
} }
} }
+7 -1
View File
@@ -1,3 +1,9 @@
# OCR decode # OCR decode
Project that takes config files in the JSON format and decodes screenshots from the F1TV data channel. Project that takes config files in the JSON format and decodes screenshots from the F1TV data channel.
If you want to use it go change thoses variables :
Reader.cs "DEBUG_DUMP_FOLDER"
Window.cs "TESS_DATA_FOLDER"