Существует ли эффективный алгоритм сегментации рукописного текста? - PullRequest
33 голосов
/ 04 ноября 2011

Я хочу автоматически разделить изображение древнего рукописного текста по строкам (и по словам в будущем).

Первая очевидная часть - это предварительная обработка изображения ...

Я просто использую простую оцифровку (в зависимости от яркости пикселя). После этого я сохраняю данные в двумерный массив.

Следующая очевидная часть - анализ двоичного массива.

  1. Мой первый алгоритм был довольно прост - если в строке массива больше черных пикселей, чем среднеквадратичное значение Максимум и Минимальное значение, тогда этот ряд является частью строки.

    После формирования списка линий я обрезал строки с высота , что меньше среднего. Наконец это превратилось в некую линейную регрессию, пытаясь минимизировать разницу между пустыми строками и строками текста. (Я предположил этот факт) First results

  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 <α1 ИЛИ x2> α2) и (1 / xo + [a1 x1] / [a2 x2])> 0 Последняя функция самая эффективная. Results with GA Фитнес-функция (1 / (HeigthRange + SpacesRange)

Где диапазон - это разница между максимумом и минимумом. Это представляет однородность текста. Глобальный оптимум этой функции - самый плавный способ разделения изображения на линии.

Я использую C # с моим самокодированным GA (классический, с 2-точечным пересечением, хромосомами с серым кодом, максимальная популяция - 40, частота мутаций - 0,05)

Теперь у меня закончились идеи, как разделить это изображение на линии с ~ 100% точностью.

Какой эффективный алгоритм для этого?


UPDATE: Исходное изображение Оригинальный BMP (1,3 МБ)


UPDATE2: Улучшены результаты по этому тексту до 100% Nev results

Как я это сделал:

  • исправлена ​​незначительная ошибка в подсчете дальности
  • изменена функция пригодности на 1 / (distanceRange + 1) * (heightsRange + 1))
  • свернута классифицирующая функция до (1 / xo + x2 / range)> 0 (точки в строке теперь не влияют на классификацию) (то есть оптимизировал входные данные и сделал оптимизацию фитнес-функций более явной)

Проблема:

Problem

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», и становится невозможно решить эту проблему с помощью поиска оптимумов. Вторая причина - этот код меняется слишком часто. Я постараюсь полностью изменить этот код, но я не знаю, как это сделать.

Q

  1. Если есть более эффективная функция фитнеса?
  2. Как найти более универсальную функцию определения?

Ответы [ 3 ]

13 голосов
/ 16 января 2012

Хотя я не уверен, как перевести следующий алгоритм в GA (и я не уверен, почему вам нужно использовать GA для этой проблемы), и я мог бы не согласиться с предложением этого, здесь идет речь.

Простая техника, которую я бы предложил, это подсчитать количество черных пикселей в строке. (На самом деле это плотность темных пикселей в ряду.) Для этого требуется очень мало операций, и с помощью нескольких дополнительных вычислений нетрудно найти пики в гистограмме суммы пикселей.

Необработанная гистограмма будет выглядеть примерно так: профиль в левой части экрана показывает количество темных пикселей в строке. Для наглядности фактическое количество нормировано для растяжения до x = 200.

raw horizontal count

После добавления некоторой дополнительной простой обработки (описанной ниже) мы можем сгенерировать подобную гистограмму, которая может быть обрезана до некоторого порогового значения. Остались пики, указывающие на центр строк текста.

processed horizontal count

Оттуда очень просто найти линии: просто обрезать (порог) гистограммы при некотором значении, таком как 1/2 или 2/3 максимума, и при необходимости проверить, что ширина пика на вашем пороге отсечения равна некоторое минимальное значение w.

Одна из реализаций полного (но все еще простого!) Алгоритма для нахождения лучшей гистограммы выглядит следующим образом:

  1. Выполняйте бинаризацию изображения с использованием порога «скользящего среднего» или аналогичного метода локальной пороговой обработки в случае, если стандартный порог Оцу, работающий с пикселями вблизи краев, не является удовлетворительным. Или, если у вас хорошее черно-белое изображение, просто используйте 128 в качестве порога бинаризации.
  2. Создать массив для хранения вашей гистограммы. Длина этого массива будет высотой изображения.
  3. Для каждого пикселя (x, y) в бинаризованном изображении найдите количество темных пикселей выше и ниже (x, y) на некотором радиусе R. То есть подсчитайте количество темных пикселей из (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» (или вдоль угла тета) и «расстоянии с предыдущего темного пикселя в 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;
6 голосов
/ 07 ноября 2011

Поработав некоторое время, я обнаружил, что мне просто нужно посчитать количество пересечений для каждой линии, то есть переход с белого на черный будет считаться одним, а переход с черного на белый будет увеличиваться наеще раз.Подсветив каждую строку со счетом> 66, я получил точность, близкую к 100%, за исключением самой нижней строки.

Конечно, не будет устойчивым к слегка повернутым отсканированным документам.И этот недостаток заключается в необходимости определения правильного порога.

image

2 голосов
/ 05 ноября 2011

ИМХО с изображением, которое было бы так сложно сделать на 100% идеально. Мой ответ - дать вам альтернативную идею.

Идея 1: Создайте свою собственную версию ReCaptcha (разместите на своем собственном сайте проны) - и сделайте это увлекательной игрой ... "Как вырезать слово (все ребра должны быть пробелами - с некоторым допуском для перекрывающихся символов в верхних и нижних строках" ). "

Идея 2: Это была игра, в которую мы играли в детстве, проволока вешалки была изогнута волнами и соединена с зуммером, и вам пришлось перемещаться по палочке с кольцом на конце, через которую провод проходил через одну сторону к другой. не заставляя зуммер сработать. Возможно, вы могли бы адаптировать эту идею и сделать мобильную игру, в которой люди будут чертить линии, не касаясь черного текста (с допуском на перекрывающиеся символы) ... когда они могут сделать линию, они получают очки и переходят на новые уровни, где вы даете им сложнее изображения ..

Идея 3: Узнайте, как Google / Recaptcha обошли это

Идея 4: Получить SDK для фотошопа и освоить его функциональность Извлечь края инструмента

Идея 5: Растяните кучу изображений по оси Y, что должно помочь, примените алгоритм, затем уменьшите измерения местоположения и примените их к изображению нормального размера.

...