Удаление фона у изображения с текстом с помощью PHP

от
PHP/MySQL   image processing, графика

Итак, волей судьбы передо мной была поставлена довольно нетривиальная задача - удалить фон у изображения, оставив при этом самое ценное - текст (будь то рукописный или печатный), печать и так далее.
Мучился я с этой ерундой несколько дней, поэтому так просто оставить все в закромах родины я не могу.

Первая мысль что приходит в голову - циклом проходить по всему изображению, узнавать цвет каждого пикселя и сравнивать его с неким числом, светлее которого пиксель считать белым, иначе копировать оригинальный. Такой код у меня остался со времен портирования конвертера шрифтов для PPM на php:
Открыть спойлер
Скормим такому коду изображение и получим... полную хрень.
old_in.jpg old_out.jpg
В действительности, нужно каждый раз угадывать то самое магическое число при котором нужное останется, а фона не станет. А иногда это вообще может не сработать, удаляется все или остается такой безобразный кусок фона. Нам такое, конечно же, не подходит.

Глава номер два, по кусочкам.
Немного погуглив на эту тематику, нашел способ с разбиванием изображения на отдельные участки (далее - секторы).
К сожалению, кроме слов, там ничего не было, поэтому код напишем мы сами. Итак, для начала нам бы пройтись по изображению, и не просто так, а сектор за сектором. Заодно запишем все цвета в каждом секторе в массив.
Открыть спойлер
Тут я немного приврал, пишем в массив не сами цвета, а среднее арифметическое между тремя каналами цвета, потому как сам цвет нам не важен, он может быть любой. Нам важно только его усредненное значение, степень "темноты", так сказать.
В итоге, пройтись по изображению по секторам смогли, все цвета достать смогли, но как узнать то самое "некое число" по которому решать что данный пиксель - фон или нет. И вот тут начинается самое интересное.

Использовать какое-то определенное число для всех секторов нельзя, результат мы уже видели. Поэтому будем его вычислять.
Цвета в секторе встречается с какой-то определенной частотой, какие-то чаще, какие-то нет. Если включить воображение, то получим на основе массива $dark_arr примерно такой график:
  graph.png
Рисовать шкалы мне было лень, поэтому дофантазируем что по горизонтали у нас цвет, а по вертикали - число встречаемости этого цвета.

На графике, как видно, есть несколько вершин, две самых больших из них - это искомый текст (ближе к началу) и фон (ближе к концу графика). Наше некое число находится примерно посередине между этими вершинами. Let's Rock!

Массив с цветами мы уже достали, осталось его подготовить к нашей процедуре, для начала хотя бы добавить недостающие цвета (точнее забить их нулями) и сгруппировать по промежуткам, сгладив таким образом график. В итоге должны остаться только самые крупные вершины.
Проходим с размахом, выходя за рамки 0-255. Почему? - позже.
Открыть спойлер
В итоге если мы снова представим график, то получим что-то такое:
   two_tops.png
Все хорошо, да не очень. Вместе с ним секторах будет встречаться еще такой график
   one_top.png
Волноваться не стоит, нужно прыгать от счастья, это тот самый сектор, в котором кроме фона ничего нет, с ним возиться нужно будет куда меньше. Его дальше будем просто закрашивать белым.

Теперь для этих вершин нужно узнать значения цвета, для того чтоб выбрать из них два самых больших и по ним узнать наше "некое" число, и заодно посчитать количество вершин - если вершина одна - просто закрасить сектор белым.
И вот если бы я не прогуливал математику, наверное, сделать бы это было просто. Но спустя пару часов (и то не сразу) я родил такой код:
Открыть спойлер
Смысл кода: сначала дважды переворачиваем массив при помощи array_flip (в камментах подсказали что для этих целей можно, и нужно использовать array_unique), тем самым удаляем все повторяющиеся значения, сортируем по ключу и перемещаем указатель массива на самое начало.
Затем извращенным циклом с указателями проходим по массиву, считываем текущее, предыдущее и следующее значение. Если текущее больше других двух, считаем ключ вершиной и записываем его в массив $max_arr.


Мы получили массив с вершинами, из них нам нужно только две самых больших, вновь это вычисляем:
Открыть спойлер

Затем такой нехитрой формулой вычисляем наше искомое число - к минимальному значению прибавляем примерно половину разницы между вершинами. Попутно добавляем коэффициент, чтоб можно было регулировать результат. Если второй вершины не было, то для $dark присваиваем ноль, тем самым далее сообщая что данный квадрат нужно закрасить белым.
  1. //Определяем порог для сектора
  2. if($max_two AND $max_one)
  3.   $dark = $max_two + (($max_one - $max_two) * 0.4);
  4. else
  5.   $dark = 0;

И проходим по сектору, закрашиваем его в соответствии с нашим найденным числом.
Открыть спойлер

Вуаля, получаем после обработки отличный, но слегка грубый результат:
   good_out.png

Сгладим это с помощью отличной либы PPMage, где собраны эффекты из ProPaintMobile портированные на php. (огромное спасибо aNNIMON'у)
Открыть спойлер

И получаем результат:
   super_good.png

То что доктор прописал!
И напоследок - рабочий пример, с оформленным классом, готовым к использованию.
Плюс специальное предложение! В архиве также нахаляву доступен эффект BoxBlur из либы PPMage.

  bgcleaner.zip

ПС: естественно, этот скрипт выжирает все предоставленные под него ресурсы. Будьте осторожны с большими картинками =)
+2   4   2
2568