提升 手写字定位 鲁棒性:多笔型输入场景下的兼容性(马克笔、铅笔、签字笔)
我们在手写字的时候,可能会使用不同的笔,比如签字笔、马克笔、炭笔、铅笔等,它们绘制的线条清晰度、颗粒质感完全不同。 一个非常明显的区别,就是绘制的字迹清晰度差异非常大:

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

但是用 亮度差异 定位会有一些明显的问题:
首先,亮度本身会损失颜色信息,比如红色的文字,转化为亮度之后,会损失红色通道数据;
其次,马克笔和铅笔的文字边缘,对比度差异巨大。
那如果我们降低亮度差异阈值呢?比如从100调整为40:
var ok = crdif > 40;

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

我们看到,出现了好的一方面是:铅笔字也被非常好地定位出来了,
但是出现一个失控的问题:纸上很小的褶皱,或者是高清拍摄的素描纸中的那种斑点的颗粒感,也别标记了出来,造成了极大的干扰,细节干扰增大。
因此,由以上我们基本上可以判断出来,使用单纯的亮度+过低的阈值,绝对是一个错误的选择:过高的阈值 或 过低的阈值,适应性都很差。
这时有两个问题需要考虑:
第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;
以下 黑色字 和 红色字 在使用 亮度差异 和 色彩差异时 的效果比对:

从这个比对我们可以看到,使用 色彩差异 能够最大限度的减少信息的损失,因此从 亮度差异 和 色彩差异 中,肯定要选择 色彩差异 来作为一个重要的决策项,
那么接下来我们要考虑另外的一个问题,就是关于 阈值的取舍。拍摄的 手写字体,在不同的环境下 亮度差异 是非常大的,光线很好的时候,拍摄的内容非常清晰,对比度非常高,反之则对比度极低,再加上 铅笔字 如果在 低亮度下,既不清晰,又对比度低,就会更难统一参数。所以这里我们必须考虑 动态参数。
那么这个 动态参数 如何来获取呢?我们能够通过某一种方式来决策得到这个具体的动态参数吗?好像也是很困难的。它也有很大的缺陷:如果拍照的图像从整体上出现了 亮度 的偏差,那么全局的动态参数会漏检很多目标。
我们的眼睛在看到不同内容的时候,是如何一眼看到差异的呢?其实是通过一种相对性,不管差异大或者是小,都可以被我们眼睛精准捕捉到,但是我们会被 差异更加明显 的吸引注意力,而忽略差异更小的内容。根据这一原理,我们只需要获取到 局部的色差差异最值 就可以了。因此,我们可以先检测所有点的外邻色彩差异,然后局部分组,仅仅关心组内的最大差异:
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;
}
}
于是得到了下面的效果:

但是这里又容易混淆的是:如果我们无差别的、按序从上到下、从左到右 进行小范围的最值的判断,在向后处理时会出现更匹配的最值,这导致我们先前的标记是错误的。在细节被放大的文字上的表现就会失控,有效内容部分被优先执行的决策覆盖了。
因此我们需要调整执行的优先级:高差异值优先处理,并且已处理过的禁止再次处理:
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;
}
}
于是得到下面的效果:

从上面可以看到,我们关注的核心的目标都被成功的标记了出来。
接下来我们要做的就是,过滤掉无效标记就可以了,而这些无效标记又是非常明显的。
在进行过滤的时候,要基于一个很大的前提:假设我们拍摄的字是在一张干净的纸上。因此在上面的有效的标记中,我们要获取到的其实就是,色彩差异 最大的那一部分,以及在它上下浮动的范围段。
那么如何获取到 色彩差异 最大的部分呢?如果简单的获取全局的最大色差,也是不可行的。因为非常可能受到局部无效颜色标记的影响,导致全局的判断失误。因此要基于一个在整体上具有绝对权威性的评判标准:就是我们识别到的目标物所占的 空间比例 ,使用占有基本全局范围(不是全部,因为要可以舍弃可能都干扰杂质)的空间内有效点来取最大色差,然后以该色差作为标准,向下兼容,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();

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

本文尝试对签字笔、马克笔、铅笔不同的笔类型、不同亮度下,在素描纸上写的字抠像定位时,通过制定一些尽量抽象的决策规则,来提高对不同环境、不同噪声下的兼容性。
浙公网安备 33010602011771号