using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; namespace Test_Merge { 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(); switch (type) { case WindowType.LapTime: outputBitmap = Tresholding(outputBitmap, 185); outputBitmap = Resize(outputBitmap, 2); outputBitmap = Dilatation(outputBitmap, 1); outputBitmap = Erode(outputBitmap, 1); break; case WindowType.Text: outputBitmap = InvertColors(outputBitmap); outputBitmap = Tresholding(outputBitmap, 165); outputBitmap = Resize(outputBitmap, 2); outputBitmap = Dilatation(outputBitmap, 1); break; case WindowType.Tyre: outputBitmap = RemoveUseless(outputBitmap); outputBitmap = Resize(outputBitmap, 4); outputBitmap = Dilatation(outputBitmap, 1); break; default: outputBitmap = Tresholding(outputBitmap, 165); outputBitmap = Resize(outputBitmap, 4); 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); pixel[0] = pixel[1] = pixel[2] = (byte)gray; } } } 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 + 15 || G >= F1TV_BACKGROUND_TRESHOLD.G + 15 || B >= F1TV_BACKGROUND_TRESHOLD.B + 15) { pixel[0] = 0xFF; pixel[1] = 0xFF; pixel[2] = 0xFF; } } } inputBitmap.UnlockBits(bmpData); return inputBitmap; } /// /// 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, Convert.ToInt32((float)totR / (float)totPixels), Convert.ToInt32((float)totG / (float)totPixels), Convert.ToInt32((float)totB / (float)totPixels)); } /// /// 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; } } }