提升 手写字定位 鲁棒性:多笔型输入场景下的兼容性(马克笔、铅笔、签字笔)

我们在手写字的时候,可能会使用不同的笔,比如签字笔、马克笔、炭笔、铅笔等,它们绘制的线条清晰度、颗粒质感完全不同。 一个非常明显的区别,就是绘制的字迹清晰度差异非常大:

7cf1f5000a5c4f83b440f22910c78e16

对于这些不同类型的图像,我们在对文字抠像处理的时候,最简单想法就是:通过字体边缘与纸张的 亮度差异 来进行定位:

var cr = lb.GetPixel(c, r).Avg();
var crrd = ps_rd_5.Select(a => lb.GetPixel(a.X + c, a.Y + r).Avg()).ToList();
var crmin = crrd.Min();
var crmax = crrd.Max();
var crdif = crmax - crmin;
var ok = crdif > 100;
if (!ok)
{
    psrednot[c, r] = true;
    continue;
}
psred[c, r] = true;

10f465f7fc3942a08c7cbd4203ad8a10

但是用 亮度差异 定位会有一些明显的问题:

首先,亮度本身会损失颜色信息,比如红色的文字,转化为亮度之后,会损失红色通道数据;

其次,马克笔和铅笔的文字边缘,对比度差异巨大。

那如果我们降低亮度差异阈值呢?比如从100调整为40:

var ok = crdif > 40;
e71f5083cfe646649e844966dd28f52f

我们看到情况有所改善,但是对于 铅笔字 仍然无法覆盖,那我们是否可以考虑继续调低阈值呢?比如从 40 降低为 20 :

var ok = crdif > 20;

95cda2ebaa05493a94f4fe0e8a62ddda

我们看到,出现了好的一方面是:铅笔字也被非常好地定位出来了,

但是出现一个失控的问题:纸上很小的褶皱,或者是高清拍摄的素描纸中的那种斑点的颗粒感,也别标记了出来,造成了极大的干扰,细节干扰增大。

因此,由以上我们基本上可以判断出来,使用单纯的亮度+过低的阈值,绝对是一个错误的选择:过高的阈值 或 过低的阈值,适应性都很差。

这时有两个问题需要考虑:

第1个问题是:阈值的高低如何取舍?

第2个问题是:我们要确保彩色的字,信息是不能损失的。

第2个问题是比较简单的,要想确保信息不损失,我们就不能仅仅按照 亮度 来处理,而是要按照像素点的三个通道的色彩总差异,就能够有很大的提升:

var psround = ps_rd_curr.Where(a => rtall.Contains(a)).ToList();

var cr = lb.GetPixel(c, r).Avg();
var crrd = psround.Select(a => lb.GetPixel(a.X + c, a.Y + r).R).ToList();
var crmin_r = crrd.Min();
var crmax_r = crrd.Max();
var crdif_r = crmax_r - crmin_r;

crrd = psround.Select(a => lb.GetPixel(a.X + c, a.Y + r).G).ToList();
var crmin_g = crrd.Min();
var crmax_g = crrd.Max();
var crdif_g = crmax_g - crmin_g;

crrd = psround.Select(a => lb.GetPixel(a.X + c, a.Y + r).B).ToList();
var crmin_b = crrd.Min();
var crmax_b = crrd.Max();
var crdif_b = crmax_b - crmin_b;

var crdif = crdif_r + crdif_g + crdif_b; 
var ok = crdif > 100;

if (!ok)
{
    psrednot[c, r] = true;
    continue;
}
psred[c, r] = true;

以下 黑色字 和 红色字 在使用 亮度差异 和 色彩差异时 的效果比对:

d9bc6dcd200c4444b0752ab609eb3d7b

从这个比对我们可以看到,使用 色彩差异 能够最大限度的减少信息的损失,因此从 亮度差异 和 色彩差异 中,肯定要选择 色彩差异 来作为一个重要的决策项,

那么接下来我们要考虑另外的一个问题,就是关于 阈值的取舍。拍摄的 手写字体,在不同的环境下 亮度差异 是非常大的,光线很好的时候,拍摄的内容非常清晰,对比度非常高,反之则对比度极低,再加上 铅笔字 如果在 低亮度下,既不清晰,又对比度低,就会更难统一参数。所以这里我们必须考虑 动态参数。

那么这个 动态参数 如何来获取呢?我们能够通过某一种方式来决策得到这个具体的动态参数吗?好像也是很困难的。它也有很大的缺陷:如果拍照的图像从整体上出现了 亮度 的偏差,那么全局的动态参数会漏检很多目标。

我们的眼睛在看到不同内容的时候,是如何一眼看到差异的呢?其实是通过一种相对性,不管差异大或者是小,都可以被我们眼睛精准捕捉到,但是我们会被 差异更加明显 的吸引注意力,而忽略差异更小的内容。根据这一原理,我们只需要获取到 局部的色差差异最值 就可以了。因此,我们可以先检测所有点的外邻色彩差异,然后局部分组,仅仅关心组内的最大差异:

var difRange = img.Height / 10;
bool[,] pt_done = new bool[lb.Width, lb.Height];
foreach (var gitem in pt_difall.GroupBy(a => new { x = a.Key.X / difRange, y = a.Key.Y / difRange }) 
    )
{
    var difmax = gitem.Max(a => a.Value);
    var ptmax = gitem.First(a => a.Value == difmax).Key;
    //指定范围内,允许与最大差异指定比例上下浮动
    var difMin = difmax * 0.33;
    foreach (var p in ptmax.RoundPsN((int)(difRange * 1.5)).Where(a => rtall.Contains(a)))
    {
        if (pt_difall.ContainsKey(p)
             && pt_difall[p] > difMin)
        {
            pt_dif_ok[p.X, p.Y] = true;
        }
        pt_done[p.X, p.Y] = true;
    }
}

于是得到了下面的效果:

ab404197581e4fc98fdffe2e739fc92b

但是这里又容易混淆的是:如果我们无差别的、按序从上到下、从左到右 进行小范围的最值的判断,在向后处理时会出现更匹配的最值,这导致我们先前的标记是错误的。在细节被放大的文字上的表现就会失控,有效内容部分被优先执行的决策覆盖了。

因此我们需要调整执行的优先级:高差异值优先处理,并且已处理过的禁止再次处理:

var difRange = img.Height / 10;
bool[,] pt_done = new bool[lb.Width, lb.Height];
foreach (var gitem in pt_difall.GroupBy(a => new { x = a.Key.X / difRange, y = a.Key.Y / difRange }) 
    .OrderByDescending(a => a.Max(b => b.Value))
    )
{
    var difmax = gitem.Max(a => a.Value);
    var ptmax = gitem.First(a => a.Value == difmax).Key;
    //指定范围内,允许与最大差异指定比例上下浮动
    var difMin = difmax * 0.33;
    foreach (var p in ptmax.RoundPsN((int)(difRange * 1.5)).Where(a => rtall.Contains(a)))
    {
        //已经被大差异的处理过了,则不再处理
        if (pt_done[p.X, p.Y]) continue;

        if (pt_difall.ContainsKey(p)
             && pt_difall[p] > difMin)
        {
            pt_dif_ok[p.X, p.Y] = true;
        }
        pt_done[p.X, p.Y] = true;
    } 
}

于是得到下面的效果:
146773395fbb477dada2dda8a3c4e862

从上面可以看到,我们关注的核心的目标都被成功的标记了出来。

接下来我们要做的就是,过滤掉无效标记就可以了,而这些无效标记又是非常明显的。

在进行过滤的时候,要基于一个很大的前提:假设我们拍摄的字是在一张干净的纸上。因此在上面的有效的标记中,我们要获取到的其实就是,色彩差异 最大的那一部分,以及在它上下浮动的范围段。

那么如何获取到 色彩差异 最大的部分呢?如果简单的获取全局的最大色差,也是不可行的。因为非常可能受到局部无效颜色标记的影响,导致全局的判断失误。因此要基于一个在整体上具有绝对权威性的评判标准:就是我们识别到的目标物所占的 空间比例 ,使用占有基本全局范围(不是全部,因为要可以舍弃可能都干扰杂质)的空间内有效点来取最大色差,然后以该色差作为标准,向下兼容,60%(稍微大些或者小些都可接受)的浮动比例,就基本能够定位到目标内容的主体。少量被剩余的部分也是环绕在目标的近距离范围内,在后续的细节修复中,只需要进行宽松条件的扩展就可以:

var bksnot2 = ImgHelper.ColorBlockLock2_8_2DArray(pt_dif_ok.Clone() as bool[,]);
var ps_bgarea2 = bksnot2.Select(a => new
{
    ps = a,
    rt = ImgHelper.psToRect(a),
    difmax = a.Where(b => pt_difall.ContainsKey(b)).Average(c => pt_difall[c])
}).ToList();
//低于40基本属于很不清晰的类型,比铅笔痕迹更轻,暂时不考虑
ps_bgarea2 = ps_bgarea2.Where(a => a.difmax > 40).ToList();
 
//ps_bgarea2 聚众后获取靠谱的最大色差,避免个别特殊情况
var area_lst = ps_bgarea2.Where(a => a.difmax > 60).
    OrderByDescending(a => a.rt.Width * a.rt.Height).ToList();
var area_done = 0;
var area_sum = area_lst.Sum(a => a.rt.Width * a.rt.Height);
var area_lst_valid = area_lst.Take(0).ToList();
foreach (var are in area_lst)
{
    if (area_done + are.rt.Width * are.rt.Height > area_sum * 0.9) break;
    area_done += are.rt.Width * are.rt.Height;
    area_lst_valid.Add(are);
}
//从安全区域列表中,计算最大色差,作为有效色差的上限
var difMaxAll = area_lst_valid.Max(a => a.difmax);
var difmin = difMaxAll * 0.6;
//排除远远低于最大色差的,或者明显不合格的
ps_bgarea2 = ps_bgarea2.Where(a => a.difmax > 60 || a.difmax > difmin).ToList();

78f49e560c004d7491d568eeb26b4bf3

以上就完成了文字内容的有效定位,它适应了不同的光照,以及不同的细节干扰。整体的兼容性有了非常大的提高。

在整个处理的过程中,我们遵循了一些非常关键的原则:

如果我们在调整参数的过程中,某个参数的上下浮动 能够轻易地对结果造成影响,那么它一定不能作为一个有效的评判策略。典型的表现就是,我们会犯嘀咕:这个参数是高一点好呢,还是低一点好呢?但凡有这个顾虑,那么这个策略点基本就是不靠谱的。
我们要确保每一个步骤的处理,都要具有最大限度的兼容性,尽量地抽象。这样涉及到的参数,才能允许它上下调整时都能轻松处于安全范围,整个策略过程才能更可靠。

posted @ 2026-08-27 11:02  行人--  阅读(4)  评论(0)    收藏  举报