是否有一种有效的手写文本分割算法?
c#
image-processing
ocr
5
0

我想自动按行(以及将来按单词)划分古代手写文字的图像。

第一个明显的部分是预处理图像...

我只是使用简单的数字化(基于像素的亮度)。之后,我将数据存储到二维数组中。

下一个显而易见的部分是分析二进制数组。

  1. 我的第一个算法非常简单-如果数组行中的黑色像素多于“ 最大值”和“ 最小值”的均方根,则该行是行的一部分。

    形成线列表后,我将高度小于平均水平的线剪掉。最终,它变成某种线性回归,试图最小化空白行和文本行之间的差异。 (我认为这个事实) 初步结果

  2. 我的第二次尝试-我尝试将GA与多种健身功能配合使用。染色体包含3个值-xo,x1,x2。 xo [-1; 0] x1 [0; 0.5] x2 [0; 0.5]

确定行与行之间的标识的函数为(xo +α1x1 +α2x2)> 0 ,其中α1是行中黑色像素的缩放总和,α2是行中黑色像素之间的范围的中值。 (a1,a2 [0,1])我尝试过的另一个函数是(x1 <α1OR x2>α2)(1 / xo + [a1 x1] / [a2 x2])> 0最后一个函数是最高效。 GA结果适应度函数为(1 / /(HeigthRange + SpacesRange)

范围是最大值和最小值之间的差。它代表文本的同质性。此功能的全局最优-将图像划分为线条的最平滑方法。

我将C#与我的自编码GA(经典,两点交叉,格雷码染色体,最大种群为40,突变率为0.05)一起使用

现在我没有了如何将图像以约100%的精度分成几行的想法。

什么是有效的算法来做到这一点?


更新: 原始BMP(1.3 MB)


UPDATE2:此文本的结果提高到100% Nev结果

我是怎么做到的:

  • 修复了范围计数中的小错误
  • 将适应度功能更改为1 /(distancesRange + 1)*(heightsRange + 1))
  • 将分类函数最小化为(1 / xo + x2 / range)> 0(行中的点现在不影响分类)(即优化输入数据并使适应度函数优化更明确)

问题:

问题

GA出人意料地未能识别这条线。我查看了“发现”功能的调试数据,发现在“无法识别”的地方有太多的噪音。功能代码如下:

public double[] Ranges()
{
    var ranges = new double[_original.Height];

    for (int y = 0; y < _original.Height; y++ )
    {
        ranges[y] = 0;
        var dx = new List<int>();
        int last = 0;
        int x = 0; 

        while (last == 0 && x<_original.Width)
        {
            if (_bit[x, y])
                last = x;
            x++;
        }

        if (last == 0)
        {
            ranges[y] = 0;
            continue;
        }

        for (x = last; x<_original.Width; x++)
        {
            if (!_bit[x, y]) continue; 

            if (last != x - 1)
            {
                dx.Add((x-last)+1);
            }
            last = x;
        }
        if (dx.Count > 2)
        {
            dx.Sort();
            ranges[y] = dx[dx.Count / 2];
            //ranges[y] = dx.Average();
        }
        else
            ranges[y] = 0;
    }

    var maximum = ranges.Max();
    for (int i = 0; i < ranges.Length; i++)
    {
        if (Math.Abs(ranges[i] - 0) < 0.9)
            ranges[i] = maximum;
    }
    return ranges;
}

我在这段代码中使用了一些技巧。主要原因-我想最小化最接近的黑色像素之间的范围,但是如果没有像素,则该值变为'0',并且无法通过发现最优值来解决此问题。第二个原因-此代码更改过于频繁。我将尝试完全更改此代码,但我不知道如何执行。

问:

  1. 是否有更有效的健身功能?
  2. 如何找到更通用的确定功能?
参考资料:
Stack Overflow
收藏
评论
共 3 个回答
高赞 时间 活跃

恕我直言,显示的图像将很难做到100%完美。我的答案是为您提供其他想法。

想法1:制作自己的ReCaptcha版本(放在自己的pron网站上),并使其成为一个有趣的游戏。“就像切出一个字(边缘应全部为空格-对上面重叠的字符有一定的容忍度)和下面的行)。”

理念2:这是我们小时候玩的游戏,衣架的电线全都弯曲成波浪形,并连接到蜂鸣器,您必须操纵一根魔杖,末端有一个环,电线穿过它,在一侧而不使蜂鸣器响起。也许您可以适应这个想法并制作一个手机游戏,使人们可以在不触碰黑色文字的情况下划出界线(可以忍受重叠的字符)...当他们可以划一条线时,他们就可以获得积分并达到了新的关卡,您会更加努力图片..

想法3:研究Google / recaptcha如何解决它

理念4:获取用于Photoshop的SDK并掌握其功能Extract Edges工具

想法5:在Y轴上拉伸图像堆,这应该会有所帮助,应用算法,然后减少位置测量值并将其应用于正常大小的图像。

收藏
评论

经过一段时间的摆弄之后,我发现我只需要计算每条线的交叉点数,即,从白色到黑色的转换将被视为一个,从黑色到白色的转换将再次增加一个。通过高亮显示计数> 66的每一行,除最底行外,我的准确性接近100%。

当然,对于略微旋转的扫描文档来说,鲁棒性不强。并且存在需要确定正确的阈值的缺点。

收藏
评论

尽管我不确定如何将以下算法转换为GA(并且不确定为什么需要使用GA来解决此问题),但我可能在提出该建议时有些偏离。

我建议的简单技术是计算每行黑色像素的数量。 (实际上,这是每行的暗像素密度。)这需要很少的操作,并且通过一些额外的计算,在像素总和直方图中找到峰值并不难。

原始直方图将如下所示,其中左侧的配置文件显示一行中暗像素的数量。为了提高可视性,将实际计数标准化为x = 200。

原始水平计数

添加一些简单的处理后(如下所述),我们可以生成这样的直方图,该直方图可以裁剪为某个阈值。剩下的是指示文本行中心的峰。

处理水平计数

从那里找到线很简单:只需将直方图裁剪(阈值)为某个最大值(例如最大值为1/2或2/3),然后选择检查裁剪阈值处的峰宽是否为最小值w。

查找更好的直方图的完整算法(但仍然很简单!)的一种实现如下:

  1. 万一在边缘附近的像素上操作的标准Otsu阈值不令人满意,请使用“移动平均”阈值或类似的局部阈值技术对图像进行二值化处理。或者,如果您有一个不错的黑白图像,只需使用128作为二值化阈值。
  2. 创建一个数组来存储直方图。该数组的长度将是图像的高度。
  3. 对于二值化图像中的每个像素(x,y),找到某个半径R处(x,y)上方和下方的暗像素数量。即,计算从(x,y-R)到x(y + R)(含)。
  4. 如果垂直半径R内的暗像素数量等于或大于R(即至少一半像素是暗的),则像素(x,y)具有足够的垂直暗邻居。增加第y行的箱数。
  5. 沿着每一行前进时,跟踪具有足够邻居的像素的最左和最右x值。只要宽度(右-左+ 1)超过某个最小值,就将深色像素的总数除以该宽度。这样可以对计数进行归一化,以确保包括文本的最后一行在内的短行。
  6. (可选)平滑生成的直方图。我只是在3行中使用了均值。

“垂直计数”(第3步)消除了恰好位于文本中心线上方或下方的水平笔划。一个更复杂的算法将直接在(x,y)的上方和下方进行检查,还可以在左上方,右上方,左下方和右下方进行检查。

通过使用C#进行的粗略实现,我能够在不到75毫秒的时间内处理图像。在C ++中,通过一些基本的优化,我毫不怀疑可以大大减少时间。

此直方图方法假定文本为水平。由于该算法相当快,因此您可能有足够的时间以与水平方向每5度为增量计算像素计数直方图。具有最大峰谷差异的扫描方向将指示旋转。

我对GA术语不熟悉,但是如果我建议的内容有价值,我相信您可以将其转换为GA术语。无论如何,无论如何我都对这个问题感兴趣,所以我不妨分享一下。

编辑:也许是为了使用GA,最好考虑“距X之前一个暗像素的距离”(或沿角度theta)和“距Y之前一个暗像素的距离”(或沿角度[theta-pi / 2] )。您还可以检查所有径向上从白色像素到深色像素的距离(以查找循环)。

byte[,] arr = get2DArrayFromBitamp();   //source array from originalBitmap
int w = arr.GetLength(0);               //width of 2D array
int h = arr.GetLength(1);               //height of 2D array

//we can use a second 2D array of dark pixels that belong to vertical strokes
byte[,] bytes = new byte[w, h];         //dark pixels in vertical strokes


//initial morph
int r = 4;        //radius to check for dark pixels
int count = 0;    //number of dark pixels within radius

//fill the bytes[,] array only with pixels belonging to vertical strokes
for (int x = 0; x < w; x++)
{
    //for the first r rows, just set pixels to white
    for (int y = 0; y < r; y++)
    {
        bytes[x, y] = 255;
    }

    //assume pixels of value < 128 are dark pixels in text
    for (int y = r; y < h - r - 1; y++)
    {
        count = 0;

        //count the dark pixels above and below (x,y)
        //total range of check is 2r, from -r to +r
        for (int j = -r; j <= r; j++)
        {
            if (arr[x, y + j] < 128) count++;
        }

        //if half the pixels are dark, [x,y] is part of vertical stroke
        bytes[x, y] = count >= r ? (byte)0 : (byte)255;
    }

    //for the last r rows, just set pixels to white
    for (int y = h - r - 1; y < h; y++)
    {
        bytes[x, y] = 255;
    }
}

//count the number of valid dark pixels in each row
float max = 0;

float[] bins = new float[h];    //normalized "dark pixel strength" for all h rows
int left, right, width;         //leftmost and rightmost dark pixels in row
bool dark = false;              //tracking variable

for (int y = 0; y < h; y++)
{
    //initialize values at beginning of loop iteration
    left = 0;
    right = 0;
    width = 100;

    for (int x = 0; x < w; x++)
    {
        //use value of 128 as threshold between light and dark
        dark = bytes[x, y] < 128;  

        //increment bin if pixel is dark
        bins[y] += dark ? 1 : 0;    

        //update leftmost and rightmost dark pixels
        if (dark)
        {
            if (left == 0) left = x;    
            if (x > right) right = x;   
        }
    }

    width = right - left + 1;

    //for bins with few pixels, treat them as empty
    if (bins[y] < 10) bins[y] = 0;      

    //normalize value according to width
    //divide bin count by width (leftmost to rightmost)
    bins[y] /= width;

    //calculate the maximum bin value so that bins can be scaled when drawn
    if (bins[y] > max) max = bins[y];   
}

//calculated the smoothed value of each bin i by averaging bin i-1, i, and i+1
float[] smooth = new float[bins.Length];

smooth[0] = bins[0];
smooth[smooth.Length - 1] = bins[bins.Length - 1];

for (int i = 1; i < bins.Length - 1; i++)
{
    smooth[i] = (bins[i - 1] + bins[i] + bins[i + 1])/3;
}

//create a new bitmap based on the original bitmap, then draw bins on top
Bitmap bmp = new Bitmap(originalBitmap);

using (Graphics gr = Graphics.FromImage(bmp))
{
    for (int y = 0; y < bins.Length; y++)
    {
        //scale each bin so that it is drawn 200 pixels wide from the left edge
        float value = 200 * (float)smooth[y] / max;
        gr.DrawLine(Pens.Red, new PointF(0, y), new PointF(value, y)); 
    }
}

pictureBox1.Image = bmp;
收藏
评论
新手导航
  • 社区规范
  • 提出问题
  • 进行投票
  • 个人资料
  • 优化问题
  • 回答问题

关于我们

常见问题

内容许可

联系我们

@2020 AskGo
京ICP备20001863号