【问题标题】:Is there an efficient algorithm for segmentation of handwritten text?有没有一种有效的手写文本分割算法?
【发布时间】:2011-12-22 08:01:53
【问题描述】:

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

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

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

下一个明显的部分是分析二进制数组。

  1. 我的第一个算法非常简单 - 如果数组的一行中的黑色像素多于 MaximumMinimum 值的均方根,那么此行是行的一部分。

    在形成线条列表后,我将 height 低于平均水平的线条剪掉了。 最后它变成了某种线性回归,试图最小化空白行和文本行之间的差异。 (我假设这个事实)

  2. 我的第二次尝试 - 我尝试使用具有多个适应度函数的 GA。 染色体包含 3 个值 - xo、x1、x2。 xo [-1;0] x1 [0;0.5] x2 [0;0.5]

确定行到行身份的函数是 (xo + α1 x1 + α2 x2) > 0,其中 α1 是行中黑色像素的缩放总和,α2 是之间范围的中值行中的极端黑色像素。 (a1,a2 [0,1]) 我尝试的另一个函数是 (x1 α2)(1/xo + [a1 x1] / [a2 x2] ) > 0 最后一个功能是最有效的。 适应度函数为 (1 / (HeigthRange + SpacesRange)

其中范围是最大值和最小值之间的差异。它代表了文本的同质性。该函数的全局最优——将图像划分为线条的最平滑方式。

我正在使用 C# 和我的自编码 GA(经典,具有 2 点交叉,格雷码染色体,最大种群为 40,突变率为 0.05)

现在我想不出如何以约 100% 的准确率将此图像划分为线条。

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


更新: Original BMP (1.3 MB)


更新 2: 将此文本的结果改进为 100%

我是怎么做到的:

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

问题:

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”,并且无法通过找到最佳值来解决此问题。第二个原因 - 此代码更改过于频繁。 我将尝试完全更改此代码,但我不知道该怎么做。

问:

  1. 是否有更高效的健身功能?
  2. 如何找到更通用的测定函数?

【问题讨论】:

  • 我知道SIFT已经成功用于手写文本分割,但我没有动手经验。
  • 我是算法新手,但我想我发现了一些讨论使用隐藏马尔可夫模型进行文本识别的网站。如果它可以识别文本,也许它也可以识别空格/新词......
  • 我发现这个链接有一些代码..并没有完全按照您的要求做,但可能会给您一个想法,然后您可以根据需要对其进行修改。 codeproject.com/Articles/69647/Hidden-Markov-Models-in-C
  • 请发一张明文的图片(没有你的加工标记),我们可以玩一下
  • @Ernado 文本识别的一个重要部分是文本分割。如果您点击“版本”,您会发现大约 25-30% 的出版物可以下载为 pdf 格式。

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


【解决方案1】:

虽然我不确定如何将以下算法转换为 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 行的 bin 计数。
  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;

【讨论】:

  • 感谢您的回答。我不明白如何计算 R。它是一些常数?
  • 不客气。根据您的图像,我选择了 4 像素的 R。您可以测试几个不同的 R 值。与其使用某个固定的半径值,不如确定当前像素与其上方最近的暗像素之间的垂直距离(在 -y 方向上)。
  • 粗略猜测,您可能会自动将 R(+/- 垂直搜索半径)计算为暗像素未中断垂直运行的中值高度的一部分。在文本行中,许多垂直笔划的高度大致相同。
  • 获得原始直方图计数后,您希望将该 bin 的原始总和除以该行中暗像素占据的宽度。例如,如果在 x = 100 处遇到第一个暗像素,在 x = 250 处遇到一行中的最后一个暗像素,则通过将原始计数除以宽度 150 (= 250 - 100) 来标准化 bin .我认为,我还使用了大约 50 的宽度的最小值,以确保小笔划不会产生非常大的 bin 计数。
  • 已添加代码。这是我能接受的。祝你好运!
【解决方案2】:

在摆弄了一会儿之后,我发现我只需要计算每条线的交叉次数,也就是说,从白色到黑色的切换将计为一个,从黑色到白色的切换将增加再一次。通过突出显示计数 > 66 的每一行,我得到了接近 100% 的准确度,除了最底部的行。

当然,对于稍微旋转的扫描文档来说,它是不可靠的。并且存在需要确定正确阈值的缺点。

【讨论】:

  • 谢谢。我很快就会尝试这种方法。 GA 可以确定“良好”的分割,并有望提供 100% 的准确度。
【解决方案3】:

恕我直言,显示的图像很难做到 100% 完美。 我的回答是给你另一种想法。

想法 1: 制作你自己的 ReCaptcha 版本(放在你自己的 pron 网站上)——让它成为一个有趣的游戏。“就像剪掉一个词(边缘都应该是空白——对上下线的重叠字符有一定的容忍度) )。”

想法 2: 这是我们小时候玩的游戏,衣架的电线都弯曲成波浪状并连接到蜂鸣器,你必须用一根末端有环的魔杖导航,电线穿过它,从一边到另一边不使蜂鸣器响起。也许你可以适应这个想法并制作一个手机游戏,人们在不接触黑色文本的情况下追踪线条(允许重叠字符)......当他们可以完成线条时,他们会获得积分并进入新的水平,你会更加努力地给予他们图片..

想法 3: 研究 google/recaptcha 如何绕过它

想法 4: 获取photoshop SDK并掌握提取边缘工具的功能

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

【讨论】:

  • 谢谢。一定是离线应用,所以我会实现你的1-3个想法,当它是在线服务时,对分割速度没有要求。拉伸是一个有趣的想法。我只需要一个快速的分割,它可以找到所有的行。
  • @Ernado 欢迎并感谢您在 SO 上提出如此有趣的问题。这个社区有很多有才华的人。我希望你能得到更多的回复,因为这个话题让我很感兴趣。干杯
  • 虽然我很欣赏这个答案,但我认为有时使用算法方法来解决某些问题而不是依靠人力方法是有正当理由的,特别是如果这些问题在很大程度上可以通过算法单独解决.
  • @Hao Wooi Lim,我同意你的看法,任何使用正统方法的程序员也同意,但是这个问题很大程度上不能用算法来解决。这就是为什么恕我直言,通过让人类来做到这一点会更容易实现 100% 的准确性。
猜你喜欢
  • 1970-01-01
  • 2021-02-01
  • 2012-08-20
  • 1970-01-01
  • 1970-01-01
  • 2023-03-12
  • 2016-08-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多