You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 08:10:29