Compare commits

17 Commits

Author SHA1 Message Date
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
17 changed files with 623 additions and 385 deletions
+90
View File
@@ -0,0 +1,90 @@
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);
}
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;
}
}
public struct Tyre
{
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";
}
public override object DecodePng()
public override async Task<object> DecodePng()
{
bool result = false;
int greenValue = GetGreenPixels();
@@ -28,7 +28,7 @@ namespace OCR_Decode
if (greenValue > EmptyDrsGreenValue + EmptyDrsGreenValue / 100 * 30)
result = true;
WindowImage.Save(Reader.DEBUG_DUMP_FOLDER + "DRS" + result + ".png");
//WindowImage.Save(Reader.DEBUG_DUMP_FOLDER + "DRS" + result + ".png");
return result;
}
@@ -68,7 +68,6 @@ namespace OCR_Decode
}
public Rectangle GetBox()
{
var tessImage = Pix.LoadFromMemory(ImageToByte(WindowImage));
Engine.SetVariable("tessedit_char_whitelist", "");
Page page = Engine.Process(tessImage);
+3 -2
View File
@@ -4,6 +4,7 @@ using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tesseract;
namespace OCR_Decode
{
@@ -13,9 +14,9 @@ namespace OCR_Decode
{
Name = "Gap to leader";
}
public override object DecodePng()
public override async Task<object> DecodePng()
{
int result = GetTimeFromPng(WindowImage, OcrImage.WindowType.Gap);
int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Gap, Engine);
return result;
}
}
+2 -2
View File
@@ -14,9 +14,9 @@ namespace OCR_Decode
{
Name = "Lap time";
}
public override object DecodePng()
public override async Task<object> DecodePng()
{
int result = GetTimeFromPng(WindowImage,OcrImage.WindowType.LapTime);
int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.LapTime,Engine);
return result;
}
}
+2 -2
View File
@@ -15,10 +15,10 @@ namespace OCR_Decode
{
Name = "Name";
}
public override object DecodePng(List<string> DriverList)
public override async Task<object> DecodePng(List<string> DriverList)
{
string result = "";
result = GetStringFromPng(WindowImage);
result = await GetStringFromPng(WindowImage,Engine);
if (!IsADriver(DriverList, result))
{
+5 -4
View File
@@ -4,6 +4,7 @@ using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tesseract;
namespace OCR_Decode
{
@@ -13,10 +14,10 @@ namespace OCR_Decode
{
Name = "Position";
}
public override object DecodePng()
public override async Task<object> DecodePng()
{
string result = GetStringFromPng(WindowImage,"0123456789");
string result = await GetStringFromPng(WindowImage,Engine, "0123456789");
int position;
try
{
@@ -25,7 +26,7 @@ namespace OCR_Decode
catch
{
position = -1;
WindowImage.Save(Reader.DEBUG_DUMP_FOLDER + "FailedPosition"+".png");
//WindowImage.Save(Reader.DEBUG_DUMP_FOLDER + "FailedPosition" + ".png");
}
return position;
}
+3 -2
View File
@@ -4,6 +4,7 @@ using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tesseract;
namespace OCR_Decode
{
@@ -13,9 +14,9 @@ namespace OCR_Decode
{
Name = "Sector 1";
}
public override object DecodePng()
public override async Task<object> DecodePng()
{
int result = GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector);
int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector, Engine);
return result;
}
}
+3 -2
View File
@@ -4,6 +4,7 @@ using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tesseract;
namespace OCR_Decode
{
@@ -13,9 +14,9 @@ namespace OCR_Decode
{
Name = "Sector 2";
}
public override object DecodePng()
public override async Task<object> DecodePng()
{
int result = GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector);
int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector,Engine);
return result;
}
}
+4 -2
View File
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tesseract;
namespace OCR_Decode
{
@@ -13,9 +15,9 @@ namespace OCR_Decode
{
Name = "Sector 3";
}
public override object DecodePng()
public override async Task<object> DecodePng()
{
int result = GetTimeFromPng(WindowImage,OcrImage.WindowType.Sector);
int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector,Engine);
return result;
}
}
+42 -81
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
@@ -11,100 +12,71 @@ namespace OCR_Decode
internal class DriverTyresWindow : Window
{
private static Random rnd = new Random();
int seed = rnd.Next(0,10000);
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(0xd9,0xd8,0xd4);
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 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)
{
Name = "Tyres";
}
public override object DecodePng()
public override async Task<object> DecodePng()
{
//WindowImage.Save(Reader.DEBUG_DUMP_FOLDER + "testTyres" + rnd.Next(0, 1000) + ".png");
return GetTyreInfos();
return await GetTyreInfos();
}
private Tyre GetTyreInfos()
private async Task<Tyre> GetTyreInfos()
{
Bitmap tyreZone = GetSmallBitmapFromBigOne(WindowImage, FindTyreZone());
tyreZone.Save(Reader.DEBUG_DUMP_FOLDER + "ZONE" + ".png");
//tyreZone.Save(Reader.DEBUG_DUMP_FOLDER + "NewTyreZone_"+seed+".png");
Tyre.Type type = Tyre.Type.Undefined;
type = GetTyreTypeFromColor(OcrImage.GetAvgColorFromBitmap(tyreZone));
int laps = -1;
//MHIWsmhiw so we can detect Soft Medium Hard Inters and Wet tyres
string text = GetStringFromPng(tyreZone, "SMHIW", OcrImage.WindowType.Tyre);
if (text.Length == 1 && text != "")
string number = await GetStringFromPng(tyreZone, Engine, "0123456789", OcrImage.WindowType.Tyre);
try
{
//We found a tire letter
laps = Convert.ToInt32(number);
}
catch
{
//We could not convert the number so its a letter so its 0 laps old
laps = 0;
text = text.ToUpper();
switch (text[0])
{
case 'S':
type = Tyre.Type.Soft;
break;
case 'M':
type = Tyre.Type.Medium;
break;
case 'H':
type = Tyre.Type.Hard;
break;
case 'I':
type = Tyre.Type.Inter;
break;
case 'W':
type = Tyre.Type.Wet;
break;
default:
type = Tyre.Type.Undefined;
break;
}
}
else
{
string number = GetStringFromPng(tyreZone, "0123456789", OcrImage.WindowType.Tyre);
try
{
laps = Convert.ToInt32(number);
}
catch
{
laps = -1;
}
type = GetTyreTypeFromColor(OcrImage.GetAvgColorFromBitmap(OcrImage.Resize(tyreZone)));
}
tyreZone.Save(Reader.DEBUG_DUMP_FOLDER + "Tyre" + type + "Laps" + laps + '#' + rnd.Next(0, 1000) + ".png");
return new Tyre(type,laps);
//tyreZone.Save(Reader.DEBUG_DUMP_FOLDER + "Tyre" + type + "Laps" + laps + '#' + rnd.Next(0, 1000) + ".png");
return new Tyre(type, laps);
}
private Rectangle FindTyreZone()
{
Bitmap bmp = WindowImage;
//bmp.Save(Reader.DEBUG_DUMP_FOLDER + "OldTyreZone_" + seed + ".png");
int currentPosition = bmp.Width;
int height = bmp.Height / 2;
Color limitColor = Color.FromArgb(0x50,0x50,0x50);
Color currentColor = Color.FromArgb(0,0,0);
Color limitColor = Color.FromArgb(0x50, 0x50, 0x50);
Color currentColor = Color.FromArgb(0, 0, 0);
Size newWindowSize = new Size(bmp.Height -Convert.ToInt32((float)bmp.Height / 100f * 25f),bmp.Height - Convert.ToInt32((float)bmp.Height / 100f * 35f));
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--;
currentColor = bmp.GetPixel(currentPosition,height);
currentColor = bmp.GetPixel(currentPosition, height);
}
//Its here to let the new window include a little bit of the right
int CorrectedX = currentPosition - (newWindowSize.Width) + Convert.ToInt32((float)newWindowSize.Width/100f*10f);
int CorrectedX = currentPosition - (newWindowSize.Width) + Convert.ToInt32((float)newWindowSize.Width / 100f * 10f);
int CorrectedY = Convert.ToInt32((float)newWindowSize.Height / 100f * 35f);
if (CorrectedX <= 0)
return new Rectangle(0,0,newWindowSize.Width,newWindowSize.Height);
return new Rectangle(0, 0, newWindowSize.Width, newWindowSize.Height);
return new Rectangle(CorrectedX,CorrectedY,newWindowSize.Width,newWindowSize.Height);
return new Rectangle(CorrectedX, CorrectedY, newWindowSize.Width, newWindowSize.Height);
}
//This method has been created with the help of chatGPT
public Tyre.Type GetTyreTypeFromColor(Color inputColor)
@@ -116,6 +88,7 @@ namespace OCR_Decode
colors.Add(HARD_TYRE_COLOR);
colors.Add(INTER_TYRE_COLOR);
colors.Add(WET_TYRE_COLOR);
colors.Add(EMPTY_COLOR);
Color closestColor = colors[0];
int closestDistance = int.MaxValue;
@@ -129,6 +102,11 @@ namespace OCR_Decode
}
}
//Bitmap colorBmp = new Bitmap(200, 100);
//Graphics g = Graphics.FromImage(colorBmp);
//g.FillRectangle(new SolidBrush(closestColor), new Rectangle(0, 0, colorBmp.Width, colorBmp.Height));
//colorBmp.Save(Reader.DEBUG_DUMP_FOLDER+"AVGCOLOR_"+closestColor.ToString()+"_"+seed+".png");
//We cant use a switch as the colors cant be constants ...
if (closestColor == SOFT_TYRE_COLOR)
type = Tyre.Type.Soft;
@@ -140,27 +118,10 @@ namespace OCR_Decode
type = Tyre.Type.Inter;
if (closestColor == WET_TYRE_COLOR)
type = Tyre.Type.Wet;
if (closestColor == EMPTY_COLOR)
return Tyre.Type.Undefined;
return type;
}
}
struct Tyre
{
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;
}
}
}
+24 -4
View File
@@ -7,6 +7,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Diagnostics;
namespace OCR_Decode
{
@@ -17,6 +18,8 @@ namespace OCR_Decode
const string TEMPORARY_IMAGE_FOLDER = @"C:\Users\Moi\Pictures\SeleniumScreens\DataSetHighRes\";
int pageNmbr = 82;
Reader Reader;
Stopwatch stopWatch = new Stopwatch();
int LoadMS = 0;
public Form1()
{
InitializeComponent();
@@ -24,17 +27,34 @@ namespace OCR_Decode
private void Form1_Load(object sender, EventArgs e)
{
stopWatch.Start();
Reader = new Reader(CONFIG_FILE,TEMPORARY_IMAGE_FOLDER);
stopWatch.Stop();
LoadMS = (int)stopWatch.ElapsedMilliseconds;
RefreshUi();
}
private void RefreshUi()
private async void RefreshUi()
{
tbxResult.Text = "";
stopWatch.Restart();
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)
+1
View File
@@ -107,6 +107,7 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="DriverData.cs" />
<Compile Include="DriverDrsWindow.cs" />
<Compile Include="DriverGapToLeaderWindow.cs" />
<Compile Include="DriverLapTimeWindow.cs" />
+245 -147
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.Linq;
using System.Text;
@@ -14,7 +20,10 @@ namespace OCR_Decode
//Just so you know
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
{
LapTime,
@@ -23,62 +32,116 @@ namespace OCR_Decode
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)
{
Bitmap result = (Bitmap)InputImage.Clone();
Bitmap outputBitmap = (Bitmap)InputBitmap.Clone();
switch (type)
{
case WindowType.LapTime:
result = Grayscale(result);
result = InvertColors(result);
result = Tresholding(result, 165);
result = Resize(result);
result = Resize(result);
if (DumpDebugImages)
outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "LapTimeBefore_" + salt + ".png");
outputBitmap = Tresholding(outputBitmap, 185);
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;
case WindowType.Text:
result = Grayscale(result);
result = InvertColors(result);
result = Tresholding(result, 165);
result = Resize(result);
result = Dilatation(result, 1);
if (DumpDebugImages)
outputBitmap.Save(Reader.DEBUG_DUMP_FOLDER + "TextBefore_" + salt + ".png");
outputBitmap = InvertColors(outputBitmap);
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:
//result = RemoveBG(result);
result = RemoveUseless(result);
result = Resize(result);
result = Resize(result);
result = Resize(result);
result = Dilatation(result, 1);
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;
default:
result = Tresholding(result, 165);
result = Resize(result);
result = Resize(result);
result = Erode(result, 1);
outputBitmap = Tresholding(outputBitmap, 165);
outputBitmap = Resize(outputBitmap, 4);
outputBitmap = Erode(outputBitmap, 1);
break;
}
//result = Dilatation(result, 1);
return result;
return outputBitmap;
}
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, bmp.Width, bmp.Height);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
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 < bmp.Height; y++)
for (int y = 0; y < inputBitmap.Height; y++)
{
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);
@@ -86,63 +149,74 @@ namespace OCR_Decode
byte green = pixel[1];
byte red = pixel[2];
//Those a specific values to correct the weights so its more pleasing to the human eye
int gray = (int)(red * 0.3 + green * 0.59 + blue * 0.11);
pixel[0] = pixel[1] = pixel[2] = (byte)gray;
}
}
}
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, bmp.Width, bmp.Height);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
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 < bmp.Height; y++)
int bmpHeight = inputBitmap.Height;
int bmpWidth = inputBitmap.Width;
Parallel.For(0, bmpHeight, y =>
{
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 blue = pixel[0];
byte green = pixel[1];
byte red = pixel[2];
//Those a specific values to correct the weights so its more pleasing to the human eye
int gray = (int)(red * 0.3 + green * 0.59 + blue * 0.11);
int value = gray < threshold ? 0 : 255;
pixel[0] = pixel[1] = pixel[2] = (byte)value;
}
}
});
}
bmp.UnlockBits(bmpData);
inputBitmap.UnlockBits(bmpData);
return bmp;
return inputBitmap;
}
public static Bitmap RemoveBG(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)
{
Color limitColor = Color.FromArgb(0x50, 0x50, 0x50);
Bitmap bmp = input;
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
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 < bmp.Height; y++)
for (int y = 0; y < inputBitmap.Height; y++)
{
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);
@@ -150,25 +224,29 @@ namespace OCR_Decode
int G = pixel[1];
int R = pixel[2];
if (R <= limitColor.R && G <= limitColor.G && B <= limitColor.B)
if (R <= F1TV_BACKGROUND_TRESHOLD.R && G <= F1TV_BACKGROUND_TRESHOLD.G && B <= F1TV_BACKGROUND_TRESHOLD.B)
pixel[0] = pixel[1] = pixel[2] = 0;
}
}
}
bmp.UnlockBits(bmpData);
inputBitmap.UnlockBits(bmpData);
return bmp;
return inputBitmap;
}
public unsafe static Bitmap RemoveUseless(Bitmap bmp)
/// <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)
{
bmp = RemoveBG(bmp);
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
//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 < bmp.Height; y++)
for (int y = 0; y < inputBitmap.Height; y++)
{
byte* currentLine = ptr + (y * bmpData.Stride);
@@ -176,7 +254,7 @@ namespace OCR_Decode
bool fromBorder = true;
for (int x = 0; x < bmp.Width; x++)
for (int x = 0; x < inputBitmap.Width; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
@@ -184,17 +262,21 @@ namespace OCR_Decode
int G = pixel[1];
int R = pixel[2];
if (fromBorder && B < 0x10 && G < 0x10 && R < 0x10)
if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R)
{
pixelsToRemove.Add(x);
}
else
{
fromBorder = false;
if (fromBorder)
{
fromBorder = false;
pixelsToRemove.Add(x);
}
}
}
fromBorder = true;
for (int x = bmp.Width - 1; x > 0; x--)
for (int x = inputBitmap.Width - 1; x > 0; x--)
{
byte* pixel = currentLine + (x * bytesPerPixel);
@@ -202,13 +284,17 @@ namespace OCR_Decode
int G = pixel[1];
int R = pixel[2];
if (fromBorder && B < 0x10 && G < 0x10 && R < 0x10)
if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R)
{
pixelsToRemove.Add(x);
}
else
{
fromBorder = false;
if (fromBorder)
{
fromBorder = false;
pixelsToRemove.Add(x);
}
}
}
@@ -222,13 +308,11 @@ namespace OCR_Decode
}
}
//NOW REMOVING THE COLOR
Color limitColor = Color.FromArgb(0x50, 0x50, 0x50);
for (int y = 0; y < bmp.Height; y++)
//Removing the color parts
for (int y = 0; y < inputBitmap.Height; y++)
{
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);
@@ -236,9 +320,7 @@ namespace OCR_Decode
int G = pixel[1];
int R = pixel[2];
//We remove the background pixels
if (R >= limitColor.R || G >= limitColor.G || B >= limitColor.B)
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;
@@ -247,18 +329,19 @@ namespace OCR_Decode
}
}
bmp.UnlockBits(bmpData);
return bmp;
inputBitmap.UnlockBits(bmpData);
return inputBitmap;
}
public static Color GetAvgColorFromBitmap(Bitmap bmp)
/// <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)
{
Color limitColor = Color.FromArgb(0x50, 0x50, 0x50);
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
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;
@@ -269,10 +352,12 @@ namespace OCR_Decode
unsafe
{
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);
for (int x = 0; x < bmp.Width; x++)
for (int x = 0; x < bmpWidth; x++)
{
byte* pixel = currentLine + (x * bytesPerPixel);
@@ -280,9 +365,7 @@ namespace OCR_Decode
int G = pixel[1];
int R = pixel[2];
//We remove the background pixels
if (R >= limitColor.R || G >= limitColor.G || B >= limitColor.B)
if (R >= F1TV_BACKGROUND_TRESHOLD.R || G >= F1TV_BACKGROUND_TRESHOLD.G || B >= F1TV_BACKGROUND_TRESHOLD.B)
{
totPixels++;
totB += pixel[0];
@@ -290,26 +373,30 @@ namespace OCR_Decode
totR += pixel[2];
}
}
}
});
}
bmp.UnlockBits(bmpData);
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));
}
public static Bitmap InvertColors(Bitmap input)
/// <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)
{
Bitmap bmp = input;
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
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 < bmp.Height; y++)
for (int y = 0; y < inputBitmap.Height; y++)
{
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);
@@ -319,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);
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);
var resultBitmap = new Bitmap(inputBitmap.Width * resizeFactor, inputBitmap.Height * resizeFactor);
// Create a Graphics object to draw the resized image
using (var graphics = Graphics.FromImage(resultBitmap))
{
// Set the interpolation mode to high-quality bicubic
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
// Draw the resized image using the Graphics object
graphics.DrawImage(sourceBitmap, new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height));
graphics.DrawImage(inputBitmap, new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height));
}
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 dilated = Dilatation(thresholded, 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 erodedPixel = eroded.GetPixel(x, y);
@@ -368,24 +455,30 @@ namespace OCR_Decode
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)
{
output.SetPixel(x, y, Color.FromArgb(255, 0, 0));
outputBitmap.SetPixel(x, y, Color.FromArgb(255, 0, 0));
}
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];
@@ -397,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;
@@ -407,7 +500,7 @@ namespace OCR_Decode
{
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);
if (gray >= 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1)
@@ -425,21 +518,26 @@ namespace OCR_Decode
if (flag)
{
output.SetPixel(x, y, Color.FromArgb(255, 255, 255));
outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255));
}
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 Dilatation(Bitmap input, int kernelSize)
/// <summary>
/// Method that that use dilatation of the morphology of a bitmap
/// </summary>
/// <param name="inputBitmap">The bitmap you want to use dilatation on</param>
/// <param name="kernelSize">The amount of dilatation you want (be carefull its expensive on ressources)</param>
/// <returns>The Bitmap after Dilatation</returns>
public static Bitmap Dilatation(Bitmap inputBitmap, int kernelSize)
{
Bitmap output = new Bitmap(input.Width, input.Height);
Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height);
int[,] kernel = new int[kernelSize, kernelSize];
@@ -451,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;
@@ -461,7 +559,7 @@ namespace OCR_Decode
{
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);
if (gray < 128 && kernel[i + kernelSize / 2, j + kernelSize / 2] == 1)
@@ -479,16 +577,16 @@ namespace OCR_Decode
if (flag)
{
output.SetPixel(x, y, Color.FromArgb(0, 0, 0));
outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0));
}
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.Linq;
using System.Text;
@@ -7,6 +13,7 @@ using System.Drawing;
using System.IO;
using System.Text.Json;
using System.Windows.Forms;
using System.Diagnostics;
namespace OCR_Decode
{
@@ -22,7 +29,9 @@ namespace OCR_Decode
public List<string> Drivers { get => _drivers; private set => _drivers = 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_";
// 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\";
const int NUMBER_OF_DRIVERS = 20;
@@ -32,6 +41,10 @@ namespace OCR_Decode
ImagesFolder = imageFolder;
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)
{
MainZones = new List<Zone>();
@@ -115,25 +128,36 @@ namespace OCR_Decode
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);
Bitmap MainZoneImage = MainZone.ZoneImage;
List<Zone> zonesToAdd = new List<Zone>();
List<Bitmap> zonesImages = new List<Bitmap>();
for (int i = 0; i < NUMBER_OF_DRIVERS; i++)
{
Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset) /*- (i* (FirstZoneSize.Height / 32))*/);
Zone newDriverZone = new Zone(MainZone.ZoneImage, new Rectangle(tmpPos, FirstZoneSize));
newDriverZone.AddWindow(new DriverPositionWindow(newDriverZone.ZoneImage, new Rectangle(driverPositionPosition, driverPositionArea)));
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);
Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset));
Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize));
zonesToAdd.Add(newDriverZone);
zonesImages.Add(newDriverZone.ZoneImage);
}
//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");
MainZones.Add(MainZone);
}
@@ -147,6 +171,10 @@ namespace OCR_Decode
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)
{
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 = "";
ChangeImage(idImage);
List<List<Object>> mainResults = new List<List<Object>>();
List<DriverData> mainResults = new List<DriverData>();
//Decode
for (int mainZoneId = 0; mainZoneId < MainZones.Count;mainZoneId ++)
for (int mainZoneId = 0; mainZoneId < MainZones.Count; mainZoneId++)
{
switch (mainZoneId)
{
@@ -183,77 +216,41 @@ namespace OCR_Decode
//Main Zone
foreach (Zone z in MainZones[mainZoneId].Zones)
{
mainResults.Add(z.Decode(Drivers));
mainResults.Add(await z.Decode(Drivers));
}
break;
//Next there will be the title and laps added
//Next there could be a Title Zone and TrackInfoZone
}
}
//Display
foreach (List<Object> objects in mainResults)
foreach (DriverData driver in mainResults)
{
for (int objId = 0; objId < objects.Count; objId++)
{
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 += "Uses " + tyre.Coumpound + " tyre for " + tyre.NumberOfLaps + " laps";
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;
}
result += driver.ToString();
result += Environment.NewLine;
}
return result;
}
/// <summary>
/// Method that can be used to convert an amount of miliseconds into a more readable human form
/// </summary>
/// <param name="amountOfMs">The given amount of miliseconds ton convert</param>
/// <returns>A human readable string that represents the ms</returns>
public static string ConvertMsToTime(int amountOfMs)
{
//Convert.ToInt32 would round upand I dont want that
int minuts = (int)((float)amountOfMs / (1000f * 60f));
int seconds = (int)((amountOfMs - (minuts*60f*1000f))/1000);
int seconds = (int)((amountOfMs - (minuts * 60f * 1000f)) / 1000);
int ms = amountOfMs - ((minuts * 60 * 1000) + (seconds * 1000));
return minuts + ":" + seconds.ToString("00") + ":" + ms.ToString("000");
}
/// <summary>
/// Old method that can draw on an image where the windows and zones are created. mostly used for debugging
/// </summary>
/// <param name="idImage">the #id of the image we are working with</param>
/// <returns>the drawed bitmap</returns>
public Bitmap Draw(int idImage)
{
Bitmap result;
@@ -274,12 +271,6 @@ namespace OCR_Decode
int count = 0;
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);
foreach (Window w in zz.Windows)
{
+102 -46
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.Linq;
using System.Text;
@@ -16,17 +22,18 @@ namespace OCR_Decode
private Rectangle _bounds;
private Bitmap _image;
private string _name;
protected static TesseractEngine Engine;
protected TesseractEngine Engine;
public Rectangle Bounds { get => _bounds; private set => _bounds = value; }
public Bitmap Image { get => _image; set => _image = value; }
public string Name { get => _name; protected set => _name = value; }
//This will have to be changed if you want to make it run on your machine
public static DirectoryInfo TESS_DATA_FOLDER = new DirectoryInfo(@"C:\Users\Moi\Pictures\SeleniumScreens\TessData");
public Bitmap WindowImage
{
get
{
//This little trickery lets you have the image that the window sees
Bitmap sample = new Bitmap(Bounds.Width, Bounds.Height);
Graphics g = Graphics.FromImage(sample);
g.DrawImage(Image, new Rectangle(0, 0, sample.Width, sample.Height), Bounds, GraphicsUnit.Pixel);
@@ -37,40 +44,64 @@ namespace OCR_Decode
{
Image = image;
Bounds = bounds;
Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, "eng", EngineMode.Default);
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";
}
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";
}
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())
{
img.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
inputImage.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
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 = "";
int result = 0;
switch (type)
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:
@@ -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));
@@ -104,12 +135,10 @@ namespace OCR_Decode
} while (iter.Next(PageIteratorLevel.Word));
}
enhancedImage.Save(Reader.DEBUG_DUMP_FOLDER + Regex.Replace(rawResult, "[^0-9A-Za-z]", "") + ".png");
List<string> rawNumbers;
//In the gaps we can find '+' but we dont care about it its redondant
if(type == OcrImage.WindowType.Gap)
//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
@@ -128,6 +157,17 @@ namespace OCR_Decode
{
//ss:ms
result = (Convert.ToInt32(rawNumbers[0]) * 1000) + Convert.ToInt32(rawNumbers[1]);
if (result > 999999)
{
//We know that we have way too much seconds to make a minut
//Its usually because the ":" have been interpreted as a number
int minuts = (int)(rawNumbers[0][0] - '0');
// rawNumbers[0][1] should contain the : that has been mistaken
int seconds = Convert.ToInt32(rawNumbers[0][2].ToString() + rawNumbers[0][3].ToString());
int ms = Convert.ToInt32(rawNumbers[1]);
result = (Convert.ToInt32(minuts) * 1000 * 60) + (Convert.ToInt32(seconds) * 1000) + Convert.ToInt32(ms);
}
}
else
{
@@ -153,13 +193,21 @@ namespace OCR_Decode
page.Dispose();
return result;
}
public static string GetStringFromPng(Bitmap image,string allowedChars = "",OcrImage.WindowType windowType = OcrImage.WindowType.Text)
/// <summary>
/// Method that recovers strings from an image using Tesseract OCR
/// </summary>
/// <param name="WindowImage">The image of the window that contains text</param>
/// <param name="Engine">The Tesseract engine</param>
/// <param name="allowedChars">The list of allowed chars</param>
/// <param name="windowType">The type of window the text is on. Depending on the context the OCR will behave differently</param>
/// <returns>the string it found</returns>
public static async Task<string> GetStringFromPng(Bitmap WindowImage, TesseractEngine Engine, string allowedChars = "",OcrImage.WindowType windowType = OcrImage.WindowType.Text)
{
string result = "";
Engine.SetVariable("tessedit_char_whitelist", allowedChars);
Bitmap rawData = image;
Bitmap rawData = WindowImage;
Bitmap enhancedImage = new OcrImage(rawData).Enhance(windowType);
Page page = Engine.Process(enhancedImage);
@@ -170,35 +218,37 @@ namespace OCR_Decode
{
result += iter.GetText(PageIteratorLevel.Word);
} while (iter.Next(PageIteratorLevel.Word));
}
if (allowedChars.Contains("S"))
{
enhancedImage.Save(Reader.DEBUG_DUMP_FOLDER + "Tyre" + result +".png");
}
else
{
enhancedImage.Save(Reader.DEBUG_DUMP_FOLDER + result +".png");
}
}
page.Dispose();
return result;
}
protected Bitmap GetSmallBitmapFromBigOne(Bitmap bmp, Rectangle rectangle)
/// <summary>
/// Get a smaller image from a bigger one
/// </summary>
/// <param name="inputBitmap">The big bitmap you want to get a part of</param>
/// <param name="newBitmapDimensions">The dimensions of the new bitmap</param>
/// <returns>The little bitmap</returns>
protected Bitmap GetSmallBitmapFromBigOne(Bitmap inputBitmap, Rectangle newBitmapDimensions)
{
Bitmap sample = new Bitmap(rectangle.Width, rectangle.Height);
Bitmap sample = new Bitmap(newBitmapDimensions.Width, newBitmapDimensions.Height);
Graphics g = Graphics.FromImage(sample);
g.DrawImage(bmp, new Rectangle(0, 0, sample.Width, sample.Height), rectangle, GraphicsUnit.Pixel);
g.DrawImage(inputBitmap, new Rectangle(0, 0, sample.Width, sample.Height), newBitmapDimensions, GraphicsUnit.Pixel);
return sample;
}
protected static string FindClosestMatch(List<string> array, string target)
/// <summary>
/// Returns the closest string from a list of options
/// </summary>
/// <param name="options">an array of all the possibilities</param>
/// <param name="testString">the string you want to compare</param>
/// <returns>The closest option</returns>
protected static string FindClosestMatch(List<string> options, string testString)
{
var closestMatch = "";
var closestDistance = int.MaxValue;
foreach (var item in array)
foreach (var item in options)
{
var distance = LevenshteinDistance(item, target);
var distance = LevenshteinDistance(item, testString);
if (distance < closestDistance)
{
closestMatch = item;
@@ -208,39 +258,45 @@ namespace OCR_Decode
return closestMatch;
}
//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];
for (var i = 0; i <= s1.Length; i++)
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 <= s2.Length; j++)
for (var j = 0; j <= string2.Length; 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);
}
}
return d[s1.Length, s2.Length];
return d[string1.Length, string2.Length];
}
}
}
+21 -11
View File
@@ -62,20 +62,30 @@ namespace OCR_Decode
{
Windows.Add(window);
}
public virtual List<Object> Decode(List<string> drivers)
public virtual async Task<DriverData> Decode(List<string> drivers)
{
List<Object> result = new List<Object>();
foreach (Window w in Windows)
DriverData result = new DriverData();
Parallel.ForEach(Windows,async w =>
{
if (w is DriverNameWindow)
{
result.Add((w as DriverNameWindow).DecodePng(drivers));
}
else
{
result.Add(w.DecodePng());
}
}
result.Name = (string)await (w as DriverNameWindow).DecodePng(drivers);
if (w is DriverDrsWindow)
result.DRS = (bool)await (w as DriverDrsWindow).DecodePng();
if (w is DriverGapToLeaderWindow)
result.GapToLeader = (int)await (w as DriverGapToLeaderWindow).DecodePng();
if (w is DriverLapTimeWindow)
result.LapTime = (int)await (w as DriverLapTimeWindow).DecodePng();
if (w is DriverPositionWindow)
result.Position = (int)await (w as DriverPositionWindow).DecodePng();
if (w is 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;
}
}
+7 -1
View File
@@ -1,3 +1,9 @@
# 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"