基于ImageSharp的红色点阵LED显示屏图像预处理优化:移除边框干扰以提升OCR识别率
基于ImageSharp的红色点阵LED显示屏图像预处理优化:移除边框干扰以提升OCR识别率
我太懂你现在的困扰了——做MAUI的LED点阵OCR识别,结果照片里的边框、螺丝孔总在捣乱,要么清不干净这些噪音,要么把关键的LED点阵给毁了,还只能用ImageSharp,没法用OpenCV那套工具,确实挺棘手的。
咱们先分析下原来的处理流程为啥没搞定:原来的灰度→模糊→边缘检测→膨胀→阈值→反转的路子,没抓住LED是红色这个核心特征,上来就灰度化等于把红色和边框的灰度信息混在一起了,后续的膨胀反而可能把边框的噪点放大,最后自然没法精准区分点阵和干扰。
下面是我优化后的ImageSharp处理类,核心思路是先锁定红色区域,再做针对性的降噪和增强,确保LED点阵丝毫不损的同时干掉边框干扰:
using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using System.IO; using System.Threading.Tasks; public class ImageSharpOcrProcessor { public static byte[] ProcessImageForOcr(string inputImagePath, byte binaryThreshold = 64) { using var image = Image.Load<Rgba32>(inputImagePath); return ProcessImageInternal(image, binaryThreshold); } public static async Task<byte[]> ProcessImageForOcrAsync(string inputImagePath, byte binaryThreshold = 64) { return await Task.Run(() => ProcessImageForOcr(inputImagePath, binaryThreshold)); } private static byte[] ProcessImageInternal(Image<Rgba32> image, byte binaryThreshold) { // 第一步:先过滤红色区域,直接锁定LED点阵(核心优化点) FilterRedChannelOnly(image); // 第二步:转灰度(此时只有红色区域保留有效信息) image.Mutate(x => x.Grayscale()); // 第三步:轻度高斯模糊,平滑点阵的细微噪点但不模糊点阵结构 image.Mutate(x => x.GaussianBlur(1.0f)); // 第四步:自适应阈值处理(替代固定阈值,适配不同光照) ApplyAdaptiveThreshold(image, 11, 2); // 第五步:轻度膨胀,强化LED点阵的连通性,避免OCR漏识别 for (int i = 0; i < 2; i++) ApplyDilation(image, 2); // 第六步:反转图像,让点阵变成白色(符合OCR的常规输入要求) image.Mutate(x => x.Invert()); // 编码为PNG返回 using var memoryStream = new MemoryStream(); image.Save(memoryStream, new PngEncoder()); return memoryStream.ToArray(); } // 核心优化:只保留红色分量远高于绿蓝的像素,直接过滤边框/螺丝等非红色干扰 private static void FilterRedChannelOnly(Image<Rgba32> image) { image.ProcessPixelRows(accessor => { for (int y = 0; y < accessor.Height; y++) { var pixelRow = accessor.GetRowSpan(y); for (int x = 0; x < pixelRow.Length; x++) { var pixel = pixelRow[x]; // 红色分量必须显著大于绿、蓝分量(可根据实际LED颜色调整阈值) if (pixel.R > pixel.G + 80 && pixel.R > pixel.B + 80) { // 保留红色像素,转为高亮灰度 pixelRow[x] = new Rgba32(pixel.R, pixel.R, pixel.R, 255); } else { // 非红色区域直接置黑 pixelRow[x] = Rgba32.Black; } } } }); } // 自适应阈值:根据每个像素周围的区域计算阈值,适配不同光照下的图像 private static void ApplyAdaptiveThreshold(Image<Rgba32> image, int blockSize, int subtractValue) { image.ProcessPixelRows(accessor => { int halfBlock = blockSize / 2; int width = accessor.Width; int height = accessor.Height; // 预先计算每个区域的灰度平均值 int[,] sumTable = new int[height, width]; for (int y = 0; y < height; y++) { int rowSum = 0; for (int x = 0; x < width; x++) { rowSum += accessor.GetRowSpan(y)[x].R; sumTable[y, x] = rowSum + (y > 0 ? sumTable[y - 1, x] : 0); } } for (int y = 0; y < height; y++) { var pixelRow = accessor.GetRowSpan(y); for (int x = 0; x < width; x++) { // 计算当前像素的区域边界 int startY = Math.Max(0, y - halfBlock); int endY = Math.Min(height - 1, y + halfBlock); int startX = Math.Max(0, x - halfBlock); int endX = Math.Min(width - 1, x + halfBlock); // 计算区域内的总灰度值 int area = (endY - startY + 1) * (endX - startX + 1); int sum = sumTable[endY, endX]; if (startY > 0) sum -= sumTable[startY - 1, endX]; if (startX > 0) sum -= sumTable[endY, startX - 1]; if (startY > 0 && startX > 0) sum += sumTable[startY - 1, startX - 1]; // 计算自适应阈值 int threshold = (sum / area) - subtractValue; byte pixelValue = pixelRow[x].R; pixelRow[x] = pixelValue > threshold ? Rgba32.White : Rgba32.Black; } } }); } // 优化后的膨胀操作:使用小kernel,避免过度放大噪点 private static void ApplyDilation(Image<Rgba32> image, int kernelSize) { int width = image.Width; int height = image.Height; var originalPixels = new byte[width * height]; // 保存原始灰度值(仅R通道) image.ProcessPixelRows(accessor => { for (int y = 0; y < height; y++) { var rowSpan = accessor.GetRowSpan(y); for (int x = 0; x < width; x++) { originalPixels[y * width + x] = rowSpan[x].R; } } }); int halfKernel = kernelSize / 2; image.ProcessPixelRows(accessor => { for (int y = 0; y < height; y++) { var pixelRow = accessor.GetRowSpan(y); for (int x = 0; x < width; x++) { byte maxValue = 0; int startY = Math.Max(0, y - halfKernel); int endY = Math.Min(height - 1, y + halfKernel); int startX = Math.Max(0, x - halfKernel); int endX = Math.Min(width - 1, x + halfKernel); for (int ny = startY; ny <= endY; ny++) { for (int nx = startX; nx <= endX; nx++) { byte pixelValue = originalPixels[ny * width + nx]; if (pixelValue > maxValue) maxValue = pixelValue; } } pixelRow[x] = new Rgba32(maxValue, maxValue, maxValue, 255); } } }); } }
关键优化点说明:
- 红色通道优先过滤:这是最核心的一步,直接把非红色的边框、螺丝孔置黑,从源头减少噪音,避免后续处理把干扰和LED点阵混在一起。你可以根据实际LED的红色深浅调整
pixel.R > pixel.G + 80 && pixel.R > pixel.B + 80里的数值。 - 自适应阈值替代固定阈值:不同照片的光照差异大,固定阈值容易出现要么漏点阵要么留噪音的情况,自适应阈值会根据每个像素周围的区域动态计算阈值,适配性更强。
- 轻度模糊+小kernel膨胀:模糊只做轻度的,避免模糊点阵结构;膨胀用小kernel,只强化LED点阵的连通性,不会放大残留的边框噪点。
你可以根据实际拍摄的照片调整参数,比如红色过滤的差值、自适应阈值的块大小、膨胀次数,应该就能做到完全保留LED点阵同时干掉所有边框干扰了。
内容来源于stack exchange
相关产品推荐
相关产品推荐

