Мы часто видим картинки на изображениях: например, комиксы объединяют несколько картинок в одну. И если у вас есть развлекательное приложение, где люди публикуют мемы, как в нашем iFunny, вы будете сталкиваться с этим постоянно. Нейронные сети уже способны находить животных, людей или другие объекты, но что, если нам нужно найти всего лишь еще одно изображение на изображении? Давайте подробнее рассмотрим наш алгоритм, чтобы вы могли протестировать его с помощью блокнота в Google Colaboratory и даже реализовать в своем проекте.

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

Мем может включать в себя несколько изображений с текстом:

Одним из популярных видов мемов являются скриншоты постов в социальных сетях, например. из Твиттера. Картина занимает в них сравнительно небольшую площадь.

Изначально задача найти картинку на картинке возникла при отправке push-уведомлений. До появления полностраничных push-сообщений на Android все картинки в таких сообщениях были очень маленькими. Текст был нечитаем (зачем он тогда нужен пользователю?), а все объекты в изображении уведомления были малоинформативными. Они также были менее привлекательными.

Мы решили сделать ставку на картинки из мемов. Для увеличения визуальной части мы сделали алгоритм, вырезающий все кадры вокруг изображения вне зависимости от того, что на нем изображено.

Давайте посмотрим на алгоритм в действии на нескольких примерах (подробности ниже)

  1. Вот как алгоритм обработал черный фон и водяной знак приложения:

2. В следующем примере он не разделял изображения, хотя между ними была большая линия.

3. Этот способ подходит и для крупного вертикального текста, занимающего около 50% площади всего изображения. Однако алгоритм пропустил водяной знак приложения (еще предстоит работа).

4. Алгоритм блестяще обработал следующий пример, даже несмотря на то, что желаемое изображение занимало относительно небольшой процент мема. Алгоритм также обрезал белые рамки по бокам.

5. В следующем примере было сразу два фона (белый и черный), но алгоритм с этим справился. Тем не менее, он оставил белые рамки по бокам.

6. Последний пример особенно интересен, поскольку само изображение содержит много строк. Но даже это не смутило наш алгоритм. Верхняя граница немного больше, чем хотелось бы, но для такого сложного случая это отличный результат!

Напомним, что добиться таких результатов можно было без разметки.

Алгоритм

Пропустим неинформативные части кода. Вы можете сами найти и запустить их в полной реализации алгоритма. В этой статье мы остановимся только на основной идее.

Давайте возьмем это изображение со щенками в качестве примера. Он имеет текст вверху и небольшую полоску белого фона вместе с водяным знаком нашего приложения внизу.

Чтобы получить только картинку со щенками, нам нужно убрать два горизонтальных прямоугольника внизу и вверху. Основная идея состоит в том, чтобы распознать все прямые линии на изображении с помощью Преобразования Хафа, а затем выбрать те, которые ограничивают белую или черную монотонную область, которую мы будем считать фоном.

Шаг 1. Преобразование изображения в оттенки серого

Первым шагом обработки изображения здесь является преобразование его в монохромный формат. Есть много способов сделать это, но я предпочитаю получать L-канал (яркость) в цветовом пространстве Lab. Этот подход оказался лучшим в большинстве случаев. Но можно попробовать и другие подходы, скажем, V-канал (значение) цветового пространства HSV.

Чтобы устранить резкие перепады контраста, создающие ненужные линии на изображении, мы применяем фильтр Гаусса к полученному черно-белому изображению.

Здесь мы использовали функции из библиотеки OpenCV. В дополнение к изображению функция cv2.GaussianBlur принимает размер ядра по Гауссу (ширину, высоту) и стандартное отклонение по оси X (по оси Y по умолчанию оно равно 0). При использовании 0 в качестве стандартного отклонения оно рассчитывается на основе размера ядра. Используемые нами параметры основаны на субъективном опыте.

Вы можете найти и использовать аналогичные методы из библиотеки scikit-image, но будьте осторожны, так как они отличаются как результатами, так и входными параметрами. Канал Lightness результата функции skimage.color.rgb2lab находится в диапазоне [0,100], а фильтр Гаусса skimage.filters.gaussian более чувствителен к параметрам ядра. Это влияет на конечный результат.

Шаг 2. Обнаружение ребер

Преобразование Хафа, лежащее в основе нашего алгоритма, может работать только с бинарными изображениями, состоящими из фона и краев, принимающих значения 0 и 1.

Мы используем Canny Transform для создания бинарного изображения. Вкратце, этот алгоритм вычисляет градиент в функции интенсивности координаты пикселя и находит его локальные максимумы, площади которых определяются как искомый край.

Мы используем функцию Canny модуля Feature из библиотеки scikit-image для выполнения преобразования. Вы можете найти аналогичную функцию в библиотеке OpenCV, но ее реализация включает сглаживание фильтром Гаусса 5x5, что немного затрудняет контроль над ситуацией.

Шаг 3. Преобразование Хафа

Теперь нам нужно обнаружить прямые линии среди всех найденных нами линий. Прямолинейное преобразование Хафа отлично справляется с этой задачей. Алгоритм рисует линию на заданном расстоянии ρ от начала координат и под определенным углом θ к оси X, как показано ниже.

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

В нашем алгоритме мы используем эту реализацию от scikit-image, потому что ее проще настроить. Также эта библиотека предоставляет удобный метод выбора локальных максимумов, которые больше всего напоминают линии. Реализация OpenCV не имеет подобной функции.

Преобразование выводит углы и расстояния до найденных линий. Рисование этих линий немного сложнее, чем линии, к которым мы привыкли, поэтому вот дополнительный фрагмент кода.

Найденное расстояние — это гипотенуза, а положение прямой по оси у — катет, поэтому делим значение расстояния на синус полученного угла.

В результате выполнения этого кода вы увидите следующее:

Наш алгоритм нашел все нужные строки! Также определяется граница водяного знака нашего приложения в самом низу.

Шаг 4. Классификация линий

Наконец, нам нужно определить, какая линия является нижней, а какая верхней, а также удалить ненужные линии, например, ту, что над черной полосой.

Для решения этой проблемы мы добавили проверку областей выше и ниже найденных линий. Так как мы работаем с мемами и их подписями, то ожидаем, что фон за пределами картинки будет белым, а текст на нем — черным. Или наоборот: черный фон и белые буквы.

Поэтому наш алгоритм основан на проверке процентного соотношения белых и черных пикселей в разделенных областях.

В этом подходе мы использовали 3 переменные:

  • white_limit — нижний предел для определения белого цвета. Все пиксели со значением в диапазоне (230; 256) будут считаться белыми.
  • black_limit — это верхний предел для определения черного цвета. Все пиксели со значением в диапазоне (0; 35) будут считаться черными.
  • процент_лимит — минимальный процент белых/черных пикселей в области, при котором область будет считаться фоном с текстом.

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

Следующие несколько строк кода выбирают самую нижнюю строку среди верхних пределов и верхнюю строку среди нижних пределов. Мы также добавили условие для размера выбранной области. Если получившаяся вертикальная линия обрезает более 60% высоты изображения, мы считаем это ошибкой и возвращаем верхнюю или нижнюю границу исходного изображения. Этот параметр выбирается исходя из предположения о площади, занимаемой изображением мема.

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

Что дальше?

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

Чтобы обобщить наш подход для большего количества случаев, вы можете заменить проверку цвета фона в алгоритме монохромным обзором. Это позволит использовать метод независимо от цвета фона.

Автор: Ярослав Мурзаев.