Compare commits

14 Commits

14 changed files with 573 additions and 270 deletions
+13 -1
View File
@@ -1,4 +1,10 @@
using System;
/// 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;
@@ -42,6 +48,10 @@ namespace OCR_Decode
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 = "";
@@ -68,8 +78,10 @@ namespace OCR_Decode
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,
+11 -1
View File
@@ -1,4 +1,10 @@
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.Drawing;
using System.Linq;
@@ -14,6 +20,10 @@ namespace OCR_Decode
{
Name = "Gap to leader";
}
/// <summary>
/// Decodes the gap to leader using Tesseract OCR
/// </summary>
/// <returns></returns>
public override async Task<object> DecodePng()
{
int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Gap, Engine);
+11 -1
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.Drawing;
using System.Linq;
@@ -14,6 +20,10 @@ namespace OCR_Decode
{
Name = "Lap time";
}
/// <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 = await GetTimeFromPng(WindowImage, OcrImage.WindowType.LapTime,Engine);
+20 -3
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.Drawing;
using System.Linq;
@@ -15,6 +21,11 @@ namespace OCR_Decode
{
Name = "Name";
}
/// <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 = "";
@@ -27,11 +38,17 @@ namespace OCR_Decode
}
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;
//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())
result = true;
+13 -4
View File
@@ -1,4 +1,10 @@
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.Drawing;
using System.Linq;
@@ -14,19 +20,22 @@ namespace OCR_Decode
{
Name = "Position";
}
/// <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 = await GetStringFromPng(WindowImage,Engine, "0123456789");
string ocrResult = await GetStringFromPng(WindowImage,Engine, "0123456789");
int position;
try
{
position = Convert.ToInt32(result);
position = Convert.ToInt32(ocrResult);
}
catch
{
position = -1;
//WindowImage.Save(Reader.DEBUG_DUMP_FOLDER + "FailedPosition" + ".png");
}
return position;
}
+13 -3
View File
@@ -1,4 +1,10 @@
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.Drawing;
using System.Linq;
@@ -14,10 +20,14 @@ namespace OCR_Decode
{
Name = "Sector 1";
}
/// <summary>
/// Decodes the sector
/// </summary>
/// <returns>the sector time in int (ms)</returns>
public override async Task<object> DecodePng()
{
int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector, Engine);
return result;
int ocrResult = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector, Engine);
return ocrResult;
}
}
}
+14 -3
View File
@@ -1,4 +1,11 @@
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.Drawing;
using System.Linq;
@@ -14,10 +21,14 @@ namespace OCR_Decode
{
Name = "Sector 2";
}
/// <summary>
/// Decodes the sector
/// </summary>
/// <returns>the sector time in int (ms)</returns>
public override async Task<object> DecodePng()
{
int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector,Engine);
return result;
int ocrResult = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector,Engine);
return ocrResult;
}
}
}
+13 -3
View File
@@ -1,4 +1,10 @@
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.Diagnostics;
using System.Drawing;
@@ -15,10 +21,14 @@ namespace OCR_Decode
{
Name = "Sector 3";
}
/// <summary>
/// Decodes the sector
/// </summary>
/// <returns>the sector time in int (ms)</returns>
public override async Task<object> DecodePng()
{
int result = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector,Engine);
return result;
int ocrResult = await GetTimeFromPng(WindowImage, OcrImage.WindowType.Sector,Engine);
return ocrResult;
}
}
}
+42 -45
View File
@@ -1,4 +1,10 @@
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.Diagnostics;
using System.Drawing;
@@ -12,75 +18,56 @@ namespace OCR_Decode
internal class DriverTyresWindow : Window
{
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(0xd9, 0xd8, 0xd4);
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";
}
/// <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();
}
/// <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());
//tyreZone.Save(Reader.DEBUG_DUMP_FOLDER + "ZONE" + ".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 = await GetStringFromPng(tyreZone,Engine, "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 = await GetStringFromPng(tyreZone, Engine, "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);
}
/// <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()
{
Bitmap bmp = WindowImage;
@@ -106,15 +93,23 @@ namespace OCR_Decode
return new Rectangle(CorrectedX, CorrectedY, newWindowSize.Width, newWindowSize.Height);
}
//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)
{
Tyre.Type type = Tyre.Type.Undefined;
List<Color> colors = new List<Color>();
//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
//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);
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;
@@ -139,6 +134,8 @@ 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;
}
+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;
}
}
+46 -12
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;
@@ -23,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;
@@ -33,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>();
@@ -117,12 +129,22 @@ namespace OCR_Decode
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))*/);
Point tmpPos = new Point(0, FirstZonePosition.Y + i * FirstZoneSize.Height - Convert.ToInt32(i * offset));
Zone newDriverZone = new Zone(MainZoneImage, new Rectangle(tmpPos, FirstZoneSize));
Bitmap zoneImg = newDriverZone.ZoneImage;
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)));
@@ -135,7 +157,7 @@ namespace OCR_Decode
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);
}
@@ -149,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;
@@ -170,6 +196,11 @@ namespace OCR_Decode
}
}
}
/// <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 = "";
@@ -188,9 +219,8 @@ namespace OCR_Decode
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
@@ -202,6 +232,11 @@ namespace OCR_Decode
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
@@ -211,6 +246,11 @@ namespace OCR_Decode
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;
@@ -231,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)
{
+100 -43
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;
@@ -20,13 +26,14 @@ namespace OCR_Decode
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);
@@ -40,36 +47,61 @@ namespace OCR_Decode
Engine = new TesseractEngine(TESS_DATA_FOLDER.FullName, "eng", EngineMode.Default);
Engine.DefaultPageSegMode = PageSegMode.SingleLine;
}
/// <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 async Task<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 async Task<int> GetTimeFromPng(Bitmap wImage,OcrImage.WindowType type, TesseractEngine Engine)
/// <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:
@@ -78,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));
@@ -103,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
@@ -127,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
{
@@ -152,13 +193,21 @@ namespace OCR_Decode
page.Dispose();
return result;
}
public static async Task<string> GetStringFromPng(Bitmap image, TesseractEngine Engine, 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);
@@ -169,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;
@@ -207,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];
}
}
}
+25 -3
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.Linq;
using System.Text;
@@ -18,6 +24,7 @@ namespace OCR_Decode
{
get
{
//This little trickery lets you have the image that the zone 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);
@@ -29,6 +36,7 @@ namespace OCR_Decode
get { return _image; }
set
{
//It automatically sets the image for the contained windows and zones
_image = Image;
foreach (Window w in Windows)
{
@@ -54,21 +62,35 @@ namespace OCR_Decode
_image = image;
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)
{
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)
{
Windows.Add(window);
}
public virtual async Task<DriverData> 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)
{
DriverData result = new DriverData();
Parallel.ForEach(Windows,async w =>
{
// A switch would be prettier but I dont think its supported in this C# version
if (w is DriverNameWindow)
result.Name = (string)await (w as DriverNameWindow).DecodePng(drivers);
result.Name = (string)await (w as DriverNameWindow).DecodePng(driverList);
if (w is DriverDrsWindow)
result.DRS = (bool)await (w as DriverDrsWindow).DecodePng();
if (w is DriverGapToLeaderWindow)
+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"