namespace OcrDaemon; using System.Drawing; using OpenCvSharp; using OpenCvSharp.Extensions; static class ImagePreprocessor { /// /// Pre-process an image for OCR using morphological white top-hat filtering. /// Isolates bright tooltip text, suppresses dim background text visible through overlay. /// Pipeline: grayscale → morphological top-hat → Otsu binary → 2x upscale /// public static Bitmap PreprocessForOcr(Bitmap src, OcrOptions? options = null) { if (options != null && !options.Preprocess) return CloneArgb(src); int kernelSize = options?.KernelSize ?? 25; if (kernelSize < 3) kernelSize = 3; if (kernelSize % 2 == 0) kernelSize += 1; int scale = options?.Scale ?? 2; if (scale < 1) scale = 1; using var mat = BitmapConverter.ToMat(src); using var gray = new Mat(); Cv2.CvtColor(mat, gray, ColorConversionCodes.BGRA2GRAY); // Morphological white top-hat: isolates bright text on dark background // Kernel size 25x25 captures text strokes, suppresses dim background text using var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(kernelSize, kernelSize)); using var tophat = new Mat(); Cv2.MorphologyEx(gray, tophat, MorphTypes.TopHat, kernel); // Otsu binarization: automatic threshold, black text on white using var binary = new Mat(); Cv2.Threshold(tophat, binary, 0, 255, ThresholdTypes.BinaryInv | ThresholdTypes.Otsu); // 2x upscale for better LSTM recognition if (scale == 1) return BitmapConverter.ToBitmap(binary); using var upscaled = new Mat(); Cv2.Resize(binary, upscaled, new OpenCvSharp.Size(binary.Width * scale, binary.Height * scale), interpolation: InterpolationFlags.Cubic); return BitmapConverter.ToBitmap(upscaled); } private static Bitmap CloneArgb(Bitmap src) { var rect = new Rectangle(0, 0, src.Width, src.Height); return src.Clone(rect, System.Drawing.Imaging.PixelFormat.Format32bppArgb); } }