# OcrImage.cs ``` cs /// Author : Maxime Rohmer /// Date : 30/05/2023 /// File : OcrImage.cs /// Brief : Class containing all the methods used to enhance images for OCR /// Version : Alpha 1.0 using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; namespace TrackTrends { public class OcrImage { //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, Text, Sector, Gap, Tyre, } /// /// Create a new Ocr image to help enhance the given bitmap for OCR /// /// The image you want to enhance public OcrImage(Bitmap inputBitmap) { InputBitmap = inputBitmap; } /// /// Enhances the image depending on wich type of window the image comes from /// /// The type of the window. Depending on it different enhancing features will be applied /// The enhanced Bitmap public Bitmap Enhance(WindowType type = WindowType.Text) { Bitmap outputBitmap = (Bitmap)InputBitmap.Clone(); //Note : If you plan to activate all the comments that I used to debug the OCR I would advise to make sure that the debug folder exists switch (type) { case WindowType.Gap: //outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @"\raw_" + id + ".png"); outputBitmap = Tresholding(outputBitmap, 165); //outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @"\treshold_" + id + ".png"); outputBitmap = Resize(outputBitmap, 2); //outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @"\resize_" + id + ".png"); outputBitmap = Dilatation(outputBitmap, 1); //outputBitmap.Save(Window.GAPTOLEADER_DEBUG_FOLDER + @"\Final_dilatation_" + id + ".png"); break; case WindowType.Sector: //outputBitmap.Save(Window.SECTOR1_DEBUG_FOLDER + @"\raw_" + id + ".png"); outputBitmap = VanishOxyAction(outputBitmap); //outputBitmap.Save(Window.SECTOR1_DEBUG_FOLDER + @"\vanish_" + id + ".png"); outputBitmap = Tresholding(outputBitmap, 150); //outputBitmap.Save(Window.SECTOR1_DEBUG_FOLDER + @"\Final_treshold_" + id + ".png"); break; case WindowType.LapTime: //outputBitmap.Save(Window.LAPTIME_DEBUG_FOLDER + @"\raw_" + id + ".png"); outputBitmap = Tresholding(outputBitmap,185); //outputBitmap.Save(Window.LAPTIME_DEBUG_FOLDER + @"\Treshold_" + id + ".png"); outputBitmap = SobelEdgeDetection(outputBitmap); //outputBitmap.Save(Window.LAPTIME_DEBUG_FOLDER + @"\SobelDetection_" + id + ".png"); break; case WindowType.Text: //outputBitmap.Save(Window.STRING_DEBUG_FOLDER + @"\raw_" + id + ".png"); outputBitmap = Tresholding(outputBitmap, 165); //outputBitmap.Save(Window.STRING_DEBUG_FOLDER + @"\Final_treshold_" + id + ".png"); break; case WindowType.Tyre: //outputBitmap.Save(Window.TYRE_DEBUG_FOLDER + @"\raw_" + id + ".png"); outputBitmap = RemoveUseless(outputBitmap); //outputBitmap.Save(Window.TYRE_DEBUG_FOLDER + @"\uselessRemoved_" + id + ".png"); outputBitmap = Dilatation(outputBitmap, 1); //outputBitmap.Save(Window.TYRE_DEBUG_FOLDER + @"\Final_dilatation_" + id + ".png"); break; default: outputBitmap = Tresholding(outputBitmap, 165); outputBitmap = Resize(outputBitmap, 2); outputBitmap = Erode(outputBitmap, 1); break; } return outputBitmap; } /// /// Method that convert a colored RGB bitmap into a GrayScale image /// /// The Bitmap you want to convert /// The bitmap in grayscale public static Bitmap Grayscale(Bitmap inputBitmap) { Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); byte 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); //This is not a proper treshold method but it is helping the sobel edge detection if(gray <= F1TV_BACKGROUND_TRESHOLD.R) { pixel[0] = pixel[1] = pixel[2] = 0; } else { pixel[0] = pixel[1] = pixel[2] = (byte)gray; } } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Method that uses the Sobel Edge detection to outline the edges of the characters to help with the OCR /// /// The image with the sobel edge detection used /// private Bitmap SobelEdgeDetection(Bitmap grayscaleImage) { // Create a new bitmap for the edges Bitmap edgesImage = new Bitmap(grayscaleImage.Width, grayscaleImage.Height); // Define the Sobel operators // Its just a matrix that we will use on the all image int[,] sobelX = { { -1, 0, 1 }, { -2, 0, 2 }, { -1, 0, 1 } }; int[,] sobelY = { { -1, -2, -1 }, { 0, 0, 0 }, { 1, 2, 1 } }; // Apply the Sobel operators and normalize the gradients // NOTE: I dont know how easy or hard it would be to make this paralel but it could be a good idea to do so if possible. for (int y = 1; y < grayscaleImage.Height - 1; y++) { for (int x = 1; x < grayscaleImage.Width - 1; x++) { int gradientX = CalculateGradient(grayscaleImage, sobelX, x, y); int gradientY = CalculateGradient(grayscaleImage, sobelY, x, y); int gradient = (int)Math.Sqrt(gradientX * gradientX + gradientY * gradientY); // Normalize the gradient value // In some rare cases the value can exceed 255 so we limit it with the Math.Min method gradient = Math.Min(255, Math.Max(0, gradient)); edgesImage.SetPixel(x, y, Color.FromArgb(gradient, gradient, gradient)); } } return edgesImage; } /// /// Method that's here to be used by the sobel edge detection method (Chat GPT has been used for parts of this method) /// /// The input image with the grayscale processing already done /// The matrix to apply /// /// /// Returns the processed gradient private int CalculateGradient(Bitmap grayscaleImage, int[,] sobelOperator, int x, int y) { int gradient = 0; for (int j = -1; j <= 1; j++) { for (int i = -1; i <= 1; i++) { int pixelX = grayscaleImage.GetPixel(x + i, y + j).R; gradient += sobelOperator[j + 1, i + 1] * pixelX; } } return gradient; } /// /// Method that is used to whiten an image. Ignore the funny name. Its used to prevent colored text to trouble the OCR when it uses grayscaling /// /// The bitmap to vanish /// public static Bitmap VanishOxyAction(Bitmap inputBitmap) { Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { //Note : MAKE THIS PARALELL OMG WY DID I LEFT IT LIKE THAT byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int blue = (int)pixel[0]; int green = (int)pixel[1]; int red = (int)pixel[2]; int max = Math.Max(Math.Max(blue, green), red); if (max > 255 / 3) max = 255; pixel[0] = pixel[1] = pixel[2] = (byte)max; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Method that binaries the input image up to a certain treshold given /// /// the bitmap you want to convert to binary colors /// The floor at wich the color is considered as white or black /// The binarised bitmap public static Bitmap Tresholding(Bitmap inputBitmap, int threshold) { 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(); int bmpHeight = inputBitmap.Height; int bmpWidth = inputBitmap.Width; Parallel.For(0, bmpHeight, y => { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmpWidth; x++) { byte* pixel = currentLine + (x * bytesPerPixel); 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; } }); } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Method that removes the pixels that are flagged as background /// /// The bitmap you want to remove the background from /// The Bitmap without the background public static Bitmap RemoveBG(Bitmap inputBitmap) { Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R <= F1TV_BACKGROUND_TRESHOLD.R && G <= F1TV_BACKGROUND_TRESHOLD.G && B <= F1TV_BACKGROUND_TRESHOLD.B) pixel[0] = pixel[1] = pixel[2] = 0; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Method that removes all the useless things from the image and returns hopefully only the numbers /// /// The bitmap you want to remove useless things from (Expects a cropped part of the TyreWindow) /// The bitmap with (hopefully) only the digits public unsafe static Bitmap RemoveUseless(Bitmap inputBitmap) { //Note you can use something else than a cropped tyre window but I would recommend checking the code first to see if it fits your intended use Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); List pixelsToRemove = new List(); bool fromBorder = true; for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) { pixelsToRemove.Add(x); } else { if (fromBorder) { fromBorder = false; pixelsToRemove.Add(x); } } } fromBorder = true; for (int x = inputBitmap.Width - 1; x > 0; x--) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (fromBorder && B < F1TV_BACKGROUND_TRESHOLD.B && G < F1TV_BACKGROUND_TRESHOLD.G && R < F1TV_BACKGROUND_TRESHOLD.R) { pixelsToRemove.Add(x); } else { if (fromBorder) { fromBorder = false; pixelsToRemove.Add(x); } } } foreach (int pxPos in pixelsToRemove) { byte* pixel = currentLine + (pxPos * bytesPerPixel); pixel[0] = 0xFF; pixel[1] = 0xFF; pixel[2] = 0xFF; } } //Removing the color parts for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R >= F1TV_BACKGROUND_TRESHOLD.R +25|| G >= F1TV_BACKGROUND_TRESHOLD.G +25|| B >= F1TV_BACKGROUND_TRESHOLD.B +25) { pixel[0] = 0xFF; pixel[1] = 0xFF; pixel[2] = 0xFF; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Recovers the average colors from the Image. NOTE : It wont take in account colors that are lower than the background /// /// The bitmap you want to get the average color from /// The average color of the bitmap public static Color GetAvgColorFromBitmap(Bitmap inputBitmap) { Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; int totR = 0; int totG = 0; int totB = 0; int totPixels = 1; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); int bmpHeight = inputBitmap.Height; int bmpWidth = inputBitmap.Width; Parallel.For(0, bmpHeight, y => { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < bmpWidth; x++) { byte* pixel = currentLine + (x * bytesPerPixel); int B = pixel[0]; int G = pixel[1]; int R = pixel[2]; if (R >= F1TV_BACKGROUND_TRESHOLD.R || G >= F1TV_BACKGROUND_TRESHOLD.G || B >= F1TV_BACKGROUND_TRESHOLD.B) { totPixels++; totB += pixel[0]; totG += pixel[1]; totR += pixel[2]; } } }); } inputBitmap.UnlockBits(bmpData); return Color.FromArgb(255,Math.Min(Convert.ToInt32((float)totR / (float)totPixels),255), Math.Min(Convert.ToInt32((float)totG / (float)totPixels),255), Math.Min(Convert.ToInt32((float)totB / (float)totPixels),255)); } /// /// This method simply inverts all the colors in a Bitmap /// /// the bitmap you want to invert the colors from /// The bitmap with inverted colors public static Bitmap InvertColors(Bitmap inputBitmap) { Rectangle rect = new Rectangle(0, 0, inputBitmap.Width, inputBitmap.Height); BitmapData bmpData = inputBitmap.LockBits(rect, ImageLockMode.ReadWrite, inputBitmap.PixelFormat); int bytesPerPixel = Bitmap.GetPixelFormatSize(inputBitmap.PixelFormat) / 8; unsafe { byte* ptr = (byte*)bmpData.Scan0.ToPointer(); for (int y = 0; y < inputBitmap.Height; y++) { byte* currentLine = ptr + (y * bmpData.Stride); for (int x = 0; x < inputBitmap.Width; x++) { byte* pixel = currentLine + (x * bytesPerPixel); pixel[0] = (byte)(255 - pixel[0]); pixel[1] = (byte)(255 - pixel[1]); pixel[2] = (byte)(255 - pixel[2]); } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// Methods that applies Bicubic interpolation to increase the size and resolution of an image /// /// The bitmap you want to resize /// The factor of resizing you want to use. I recommend using even numbers /// The bitmap witht the new size public static Bitmap Resize(Bitmap inputBitmap, int resizeFactor) { var resultBitmap = new Bitmap(inputBitmap.Width * resizeFactor, inputBitmap.Height * resizeFactor); using (var graphics = Graphics.FromImage(resultBitmap)) { graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.DrawImage(inputBitmap, new Rectangle(0, 0, resultBitmap.Width, resultBitmap.Height)); } return resultBitmap; } /// /// method that Highlights the countours of a Bitmap /// /// The bitmap you want to highlight the countours of /// The bitmap with countours highlighted public static Bitmap HighlightContours(Bitmap inputBitmap) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); Bitmap grayscale = Grayscale(inputBitmap); Bitmap thresholded = Tresholding(grayscale, 128); Bitmap dilated = Dilatation(thresholded, 3); Bitmap eroded = Erode(dilated, 3); for (int y = 0; y < inputBitmap.Height; y++) { for (int x = 0; x < inputBitmap.Width; x++) { Color pixel = inputBitmap.GetPixel(x, y); Color dilatedPixel = dilated.GetPixel(x, y); Color erodedPixel = eroded.GetPixel(x, y); int gray = (int)(pixel.R * 0.3 + pixel.G * 0.59 + pixel.B * 0.11); int threshold = dilatedPixel.R; if (gray > threshold) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } else if (gray <= threshold && erodedPixel.R == 0) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 0, 0)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } } } return outputBitmap; } /// /// Method that that erodes the morphology of a bitmap /// /// The bitmap you want to erode /// The amount of Erosion you want (be carefull its expensive on ressources) /// The Bitmap with the eroded contents public static Bitmap Erode(Bitmap inputBitmap, int kernelSize) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); int[,] kernel = new int[kernelSize, kernelSize]; for (int i = 0; i < kernelSize; i++) { for (int j = 0; j < kernelSize; j++) { kernel[i, j] = 1; } } for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) { for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) { bool flag = true; for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) { for (int j = -kernelSize / 2; j <= kernelSize / 2; 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) { flag = false; break; } } if (!flag) { break; } } if (flag) { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } } } return outputBitmap; } /// /// Method that that use dilatation of the morphology of a bitmap /// /// The bitmap you want to use dilatation on /// The amount of dilatation you want (be carefull its expensive on ressources) /// The Bitmap after Dilatation public static Bitmap Dilatation(Bitmap inputBitmap, int kernelSize) { Bitmap outputBitmap = new Bitmap(inputBitmap.Width, inputBitmap.Height); int[,] kernel = new int[kernelSize, kernelSize]; for (int i = 0; i < kernelSize; i++) { for (int j = 0; j < kernelSize; j++) { kernel[i, j] = 1; } } for (int y = kernelSize / 2; y < inputBitmap.Height - kernelSize / 2; y++) { for (int x = kernelSize / 2; x < inputBitmap.Width - kernelSize / 2; x++) { bool flag = false; for (int i = -kernelSize / 2; i <= kernelSize / 2; i++) { for (int j = -kernelSize / 2; j <= kernelSize / 2; 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) { flag = true; break; } } if (flag) { break; } } if (flag) { outputBitmap.SetPixel(x, y, Color.FromArgb(0, 0, 0)); } else { outputBitmap.SetPixel(x, y, Color.FromArgb(255, 255, 255)); } } } return outputBitmap; } } } ```