丙烯马克笔 - 手写字 - 背景抠除 处理过程

710c1db40e894d19a9fe0b4e80a29f0b

丙烯马克笔在素描纸上手写字,质感真的非常棒。那么我们在素描纸上写的字,如何转化为一种干净的电子图呢?

现在我们仅仅对算法的过程进行展示,暂时不考虑性能问题。

手写算法的详细处理过程如下:

先识别字体边缘轮廓,根据极大的黑白亮度差异来进行定位。我们在进行参数的阈值选择的时候,我倾向于的原则是使用不敏感数值,通过使用更多的维度或某种正确的方式来绝对的降低对单一参数的敏感性依赖。比如这里亮度的差异值 crdif, 其实在 5 ~100之间都能够胜任,因此我们可使用的数值基本上可以随意:

for (int r = margin; r < lb.Height - margin; r++)
{
    for (int c = margin; c < lb.Width - margin; c++)
    {
        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;
    }
}

标记后的区域如下,可以看到非常好的把边缘区域都标记了出来:
7d8a691417f24edcb3af1bdf8d4bf02a

由这样闭合的字体轮廓,我们可以得到界限清晰的3部分:无效区域、边缘区域、和文字内部区域:

var bks = ImgHelper.ColorBlockLock2_8_2DArray(psrednot.Clone() as bool[,]);

由此我们可以获取到除了字体外边缘之外的模块的划分:一部分是白色背景区域,一部分是字体的黑色内部区域,然后根据区域的边缘点大概率亮度值可以非常明显的获取到字体的内部区域。这里用到的亮度 80,也是一个极其安全的数值,它基本在50~128之间都可以正常工作。这里最好的方式是根据图片的直方图来动态的检测两种主亮度,这里我们为了简单起见,随意使用一个绝对安全的固定数值 80; 另外,0.6的概率参数也是一个极其安全的数值,因为在理论情况下,目标区域的外周由于已经处于了文字内部,所以基本都是暗色的边缘,它的概率基本上是接近 1;反之的白色背景区域的概率基本上都接近于 0,所以这里的参数 0.6 也是极其安全:

Dictionary<List<Point>, List<Point>> bks_edge_full_dict = new Dictionary<List<Point>, List<Point>>();
foreach (var bk in bks)
{
    var ps_edge = bk.Where(a => a.RoundPsN(1).Any(b => !psrednot[b.X, b.Y])).ToList(); 
    //是否属于字体内部轮廓
    var ps_edge_ok = ps_edge.Count(a => lb.GetPixel(a.X, a.Y).Avg() < 80) > ps_edge.Count * 0.6;
    if (!ps_edge_ok) continue; 
    bks_edge_full_dict[ps_edge] = bk; 
}

这样我们就得到了文字及其近边缘区域的所有的点,然后将他们合并,标记为绿色显示:

//合并最初的边缘区域
var ps_black_all = psred.Clone() as bool[,];
foreach (var bk in bks_edge_full_dict)
{
    foreach (var p in bk.Value)
    {
        ps_black_all[p.X, p.Y] = true;
    }
} 

就可以剔除主要的白色背景的无效区域 :
7ba6466fa46f4b8a8cd329c1d9a98313

接下来我们要对字体的边缘部分进行透明过渡的处理:
91f0320a9ce04da2afacb91159aa9b88

如果我们单纯的进行二值分割,那么边缘会在不同的亮度阈值下呈现出明显的锯齿,无论这个参数设置为多少,都会有明显的锯齿。所以最理想的就是进行一种无级的转换,根据边缘过渡区域的亮度的上限和下限,与透明度进行关联映射。越接近字体亮度的越接近透明 255 ,越接近白色背景区域亮度的,越接近透明度 0,并且为了避免对字体的暗色区域进行错误的透明转化或干扰,我们可以仅针对于高于两者亮度均值的部分的像素点来进行转化:

//安全区的亮度均值,透明度 255
var light_avg_font = bks_edge_full_dict.Values.
    SelectMany(a => a).
    Average(a => lbInit.GetPixel(a.X, a.Y).Avg());
//移除区的亮度均值,透明度 0
var light_avg_remove = remove_area_lightall.Average();
var light_min = light_avg_remove;
var light_max = light_avg_font * 1.0;
var light_avg = (light_min + light_max) / 2;
var light_range = light_max - light_min + 1;
//对边缘区域进行透明度设置 
for (int r = 0; r < lb.Height; r++)
{
    for (int c = 0; c < lb.Width; c++)
    {
        if (psred[c, r])
        {
            if (lbInit.GetPixel(c, r).Avg() > light_avg)
            {
                var v = (int)((lbInit.GetPixel(c, r).Avg() - light_min) / (int)light_range * 255);
                v = Math.Max(0, Math.Min(255, v));
                lb.SetPixel(c, r, Color.FromArgb(v, lbInit.GetPixel(c, r)));
            }
            else
            {
                lb.SetPixel(c, r, Color.FromArgb(255, lbInit.GetPixel(c, r)));
            }
        }
    }
}

处理后的效果:
a8ded1568e6648bb943a44a384992a62

来看一下它在不同背景色下的抠像效果:
4753c449bdf743d6bb4539afabf5e1cc

整体看起来还不错,尤其是边缘区域,细节也都有保留,但是唯一的缺点是字体中心仍然有一些偏白的区域,它反映的是白色素描纸的底色,这里当我们更换为彩色或某种指定颜色背景的时候,其实应该要稍微的处理一下,让它保持与背景的色调的一致性,所以可以整体上让它在透明度上稍微加一些透明,这样可以与后背景稍微融合,就不会显得过于突兀。这一点是可以进一步去优化的。

posted @ 2026-08-17 08:54  行人--  阅读(6)  评论(0)    收藏  举报