Я люблю работу с изображениями. Не очень разбираюсь, но люблю. Всегда с интересом читаю статьи про методы триангуляции, детектирования границ, фильтры, перцептивные хэши, форматы изображений. Лет 10 назад даже пилил по вечерам конвертер из растра в вектор, но тот проект так и остался незаконченным.
А теперь мы с командой разрабатываем PIM-систему, это инструмент по управлению информацией о товарах. Среди задач в беклоге я нашел задачу себе по душе: попробовать реализовать массовую генерацию инфографики для маркетплейсов. Пока это экспериментальная функция, но, думаю, даже в существующем виде её можно использовать. Мы не стали её прятать за регистрацией, она доступна всем желающим. А в этой статье я хочу рассказать о подходе, который я использовал.
Идея
Если открыть Озон или Вайлдберрис, у многих товаров на главной картинке будет какой-то текст. В контексте работы с маркетплейсами это принято называть инфографикой. Думаю, вы поняли, о чем я. Если нет — вот как выглядит главная страница Озона для меня прямо сейчас.
На 9 из 10 картинок есть текст, добавленный поверх изображения.
В некоторых случаях видна индивидуальная работа человека: текст вписывается в композицию, не закрывает основной предмет. В других случаях это похоже на генерацию по шаблону, где в определенные заранее области вписывается текст по заранее определенному стилю.
Похоже, на это тратятся сотни тысяч часов живых людей. Давайте попробуем автоматизировать этот процесс. В идеале, от нас потребуется только указать ссылки на изображения и фрагменты текста, которые следует на них разместить, а алгоритм сам все сделает для сотен и тысяч картинок сразу.
Сначала сформируем требования к нашему алгоритму:
Текст должен хорошо читаться на превью в результатах поиска или рекомендациях.
Текст, по возможности, не должен перекрывать основной предмет на изображении.
Стиль должен быть нейтральным, чтобы подойти и к носкам, и к редуктору.
Но что если нейросети все это уже умеют? Сначала я попросил сделать ChatGPT работу за нас.
Вот результат:
Я сделал не одну попытку объяснить, что нужно сделать, и на русском, и на английском, но всегда получалось что-то не то.
После этого я приступил к работе над своей реализацией. Разработка заняла три недели работы по вечерам. Давайте посмотрим на результаты работы программы, а потом по шагам разберем тот подход, который я использовал. Может, результат вам не понравится, и про процесс читать не захотите. Дальше будут картинки. Их я взял с настоящих карточек товара на Озоне, оригиналы не содержат текста. Алгоритм поддерживает некоторую вариативность, из нескольких вариантов для одного изображения я выбрал лучшие на мой взгляд, но по воспринимаемому качеству они не сильно отличались.
Результат
Начнем с простого, предмет на белом фоне.
Кофеварка была по центру, алгоритм сместил её к краю, чтобы разместить надписи, при этом оставил общий размер изображения таким же. Важные значения выделил жирным и увеличил для них размер текста.
Кое-какая логика есть и в отступах и полях, но фиксированной сетки нет: цель — сделать текст читаемым при небольших размерах картинки, не перекрыть детали на изображении, не перекрыть надписи друг другом, и подружить это с сеткой у меня не получилось.
Работа с черным фоном принципиально не отличается.
Фон не обязательно должен быть однотонным, со сложным фоном алгоритм тоже справляется.
Также поддерживается перечисление доступных цветов, его часто используют продавцы чтобы показать, что есть разные варианты товара в разных цветах. Можно просто написать «синий», «зеленый», «малиновый» и даже «бежево‑красный» (затащил в проект 1200 всевозможных названий цветов), а можно указать код в виде #EBACCA. Если цветов встретилось 2 и больше, они сгруппируются и отобразятся в виде кружочков.
Еще один пример
Если надписей много, результаты выглядят примерно так:
Бывают и менее удачные варианты:
Кроме того, я попробовал запрограммировать какой-нибудь видео-эффект.
Гифки ужал до приемлемых размеров, в оригинале это видео 60 FPS.
Можете поэкспериментировать со своими изображениями и надписями на странице https://catalog.app/public-opportunities/generate-infographics.
Как это работает
В первоначальной концепции было больше плашек, полосок, наклеек, теней, обводок, градиентов. А когда я все это реализовал, оказалось, что, на мой вкус, без них лучше, и я стал их понемногу убирать. В итоге осталось несколько эффектов, которые нужны для читаемости на пестром фоне.
А поскольку эффектов планировалось много, и было много, хотелось код построить таким образом, чтобы эффекты были независимыми. Кроме того, на круглой плашке хорошо смотрится короткий текст из одного-двух коротких слов. Большим предложениям нужно больше места по горизонтали: неудобно читать предложение по одному слову на строчке. Поэтому подход к организации проекта был таким:
Есть менеджер инфографики. Он отвечает за весь процесс, но делегирует некоторые операции отдельным эффектам. В задачи менеджера входит:
Оценить свободное пространство, однородность фона и количество надписей. При необходимости — подвинуть предмет на изображении вправо или влево.
Сделать уменьшенную копию изображения, подготовить маску: изображение в градиентах серого, показывающее, где надписи размещать можно, где нельзя, а где — можно, но при условии, что нет более подходящего места.
Выделить из набора слова с обозначением цветов, чтобы отобразить их специальным образом в виде кружков.
Приоритезировать эффекты: если текста много, будем расходовать место экономно и отдадим приоритет простому тексту, если мало — можно сделать и заголовки, и плашки.
Делегировать добавление текста отдельным эффектам.
Обеспечить вариативность для того, чтобы потом можно было выбрать лучший вариант из сгенерированных.
В задачи эффекта входит:
Выбрать наиболее приоритетную надпись из подходящих (например, заголовок ограничен длиной текста)
Выбрать наиболее подходящее место из доступных.
Выбрать цвет текста, определить, нужна ли обводка или подложка (мы стараемся их избегать, но если подобрать контрастный цвет не получается — используем)
Нанести текст на слой с эффектами, а занятое пространство — на слой с маской.
Давайте посмотрим по шагам, как это происходит. Возьмем исходное изображение:
Создадим для него маску:
Рандом решил, что стоит добавить сетку. Вообще, рандом участвует в алгоритме: добавляет или не добавляет сетку, передвигает предмет вправо-влево, выбирает акцентные цвета и так далее. За счет этого обеспечивается вариативность, что позволяет сгенерировать несколько вариантов.
Дальше добавляем надписи. На маске этот процесс выглядит так:
И итоговый результат:
Давайте посмотрим еще один пример по шагам:
И результат:
Всего получилось чуть более 3000 строчек кода, поэтому весь код разобрать и прокомментировать, к сожалению, не получится. Остановлюсь на моментах, которые я счел интересными.
Во-первых, работа со шрифтами — сложный процесс. Взять файл шрифта и вручную, без библиотек, получить набор пикселей — гиблая идея. А при использовании библиотек мы ограничены их возможностями. Я использовал ImageMagick, и, по сути, там есть три метода, которые полезны в контексте задачи:
Вписать текст в прямоугольник так, чтобы размер текста был наибольшим из возможных. В этом случае ImageMagick может взять работу с переносами слов на новую строку на себя.
Узнать, прямоугольник какого размера потребуется, чтобы растеризовать определенный текст с определенным размером шрифта. В этом случае расстановка переносов строк за нами.
Растеризовать определенный текст с определенным размером шрифта.
И это не ImageMagick плох, у других библиотек схожий набор возможностей.
Это привело к тому, что некоторые надписи выглядят не так, как я хотел бы. Исправить это за приемлемое время у меня не получилось.
Во-вторых, поиск наибольшего прямоугольника в матрице из нулей и единиц — нетривиальная задача, если нужна временная сложность O(m*n). Без гугла я бы не справился.
В-третьих, работать с цветами удобнее в цветовом пространстве HSL. Там многие вещи становятся проще: и проверка достаточности контраста, и подбор цвета, сочетающегося с фоном.
В-четвертых, при работе с изображениями полезно знать про операции свертки и их применение. Например, и размытие, и детектирование границ реализуется одним методом с разными аргументами (одним из аргументов выступает матрица свертки). Полезно не в том смысле, что нужно реализовать этот метод самостоятельно, а в том, чтобы понимать базовые возможности графических библиотек. Та же свертка должна быть эффективно реализована в любой библиотеке.