是否有一种有效的手写文本分割算法?

时间:2011-11-04 19:55:31

标签: c# algorithm image-processing ocr genetic-algorithm

我想通过线条(以及将来的文字)自动划分古代手写文字的图像。

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

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

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

  1. 我的第一个算法很简单 - 如果数组的一行中的黑色像素多于最大最小的均方根值,那么这一行是行的一部分。

    在形成线条列表后,我切断了 height 小于平均值的线条。 最后它变成了某种线性回归,试图最小化空行和文本行之间的差异。 (我以为这个事实) First results

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

  3. 确定行到行的标识的函数是(xo +α1x1+α2x2)> 0 ,其中α1是行中黑色像素的缩放和,α2是行中极端黑色像素之间的范围的中值。 (a1,a2 [0,1]) 我尝试的另一个功能是(x1<α1OR x2>α2)(1 / xo + [a1 x1] / [a2 x2])> 0 最后一个功能是最有效的。 Results with GA 健身功能是 (1 /(HeigthRange + SpacesRange)

    范围是最大值和最小值之间的差异。它代表了文本的同质性。此功能的全局最佳 - 将图像划分为线条的最流畅方式。

    我使用C#和我的自编码GA(经典,2点交叉,灰色代码染色体,最大群体为40,突变率为0.05)

    现在我没有想法如何将这个图像划分为~100%准确度的行。

    执行此操作的有效算法是什么?


    更新 Original image Original BMP (1.3 MB)


    UPDATE2: 将此文本的结果改进为100% Nev results

    我是怎么做到的:

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

    问题:

    Problem

    GA令人惊讶地未能认识到这一点。我看了'find rages'函数的调试数据,发现在'无法识别'的地方有太多的噪音。 功能代码如下:

    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”,并且找不到optima就不可能解决这个问题。第二个原因 - 这段代码变化太频繁了。 我将尝试完全更改此代码,但我不知道该怎么做。

    问:

    1. 如果有更有效的健身功能?
    2. 如何找到更多功能的确定功能?

3 个答案:

答案 0 :(得分:13)

虽然我不确定如何将以下算法转换为GA(并且我不确定为什么你需要使用GA来解决这个问题),而且我可能会在提出它的基础上做出决定。

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

原始直方图看起来像这样,左侧的轮廓显示一行中的暗像素数。为了可见性,实际计数被标准化为伸展到x = 200。

raw horizontal count

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

processed horizontal count

从那里找到线条是一件简单的事情:只需将直方图剪切(阈值)设置为某个值,例如最大值的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的bin计数。
  5. 当您沿着每一行行进时,跟踪具有足够邻居的像素的最左侧和最右侧的x值。只要宽度(右 - 左+ 1)超过某个最小值,就将暗像素的总数除以此宽度。这样可以对计数进行标准化,以确保包含最后一行文本等短行。
  6. (可选)平滑生成的直方图。我只是用了超过3行的平均值。
  7. “垂直计数”(步骤3)消除了恰好位于文本中心线上方或下方的水平笔划。更复杂的算法只能直接检查上方和下方(x,y),还可以检查左上角,右上角,左下角和右下角。

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

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

    我不熟悉GA术语,但如果我所建议的有一些价值,我相信你可以把它翻译成GA术语。无论如何,我对这个问题感兴趣,所以我不妨分享。

    编辑:也许对于使用GA,最好根据“自X中前一个暗像素的距离”(或沿着角度θ)和“自Y以前的暗像素以来的距离”(或沿着角度[θ-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;
    

答案 1 :(得分:6)

在摆弄了一段时间之后,我发现我只需要计算每条线的交叉数,也就是说,从白色到黑色的切换将计为一个,从黑色到白色的切换将增加再一次。通过使用计数&gt;突出显示每一行66除了最底线以外,我的准确率接近100%。

当然,稍微旋转的扫描文档不会很健壮。并且存在需要确定正确阈值的缺点。

答案 2 :(得分:2)

  恕我直言,显示的图像很难100%完美地完成。   我的回答是给你另类的想法。

创意1: 制作你自己的ReCaptcha版本(放在你自己的网站上) - 并使它成为一个有趣的游戏......“就像切出一个单词一样(边缘应该都是白色空间 - 对上下线的重叠字符有一定的容忍度)“。

创意2: 这是一个我们作为孩子玩的游戏,衣架的电线全部弯曲成波浪并连接到蜂鸣器,你必须用一根戒指导航,最后一根电线穿过它,横过一边到另一边没有让蜂鸣器响起。也许你可以调整这个想法并创建一个移动游戏,人们可以在不触及黑色文本的情况下追踪线条(对重叠字符具有容忍度)......当他们能够排队时他们获得积分并达到新的水平,让你更难图像..

创意3: 研究google / recaptcha如何解决这个问题

创意4: 获取Photoshop的SDK并掌握它的功能Extract Edges工具

创意5: 拉伸Y轴上的图像堆应该有所帮助,应用算法,然后减少位置测量并将其应用于正常尺寸的图像。