Генерация схемы сборки картины-фотографии из LEGO


10.10.2020Познавательно


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

Проект

Проект представляет из себя сайт, на котором находится конструктор картины. В нем можно загрузить фотографию, выбрать необходимый размер, подвигать привычные нам ползунки при редактировании фотки и посмотреть на то, как картина будет выглядеть в собранном состоянии. Картина состоит из огромной кучи маленьких деталей LEGO (1x1 блок), которые необходимо собрать на доске по схеме, полученной после покупки набора.

Задача

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

Теория (как это работает в общем случае)

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

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

https://en.wikipedia.org/wiki/Indexed_color

Для решения нашей задачи необходимо получить в конечном счете изображение, которое имеет намного меньше пикселей (блоков LEGO) и, внимание, состоит из определенного перечня цветов! Никто на заводе не будет штамповать LEGO под определённый заказ пользователя.

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

Я сказал придумать? Бросьте, за нас уже давно всё придумали. То, что нам надо, называется квантование цветов. Суть проста: максимально уменьшаем количество используемых цветов в палитре изображения так, чтобы визуально было похоже на оригинал.

https://en.wikipedia.org/wiki/Color_quantization

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

Для исправления положения с резкими переходами используют дизеринг. Как и в способах квантования, так и в дизеринге есть свои алгоритмы, которым десятки лет. Один из них очень популярный алгоритм – алгоритм Флойда – Стейнберга. Его реализация занимает считанные строки кода, но даже и этого вам не надо делать! Все есть в библиотеках для работы с изображениями.

https://en.wikipedia.org/wiki/Floyd–Steinberg_dithering

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

Как это работает в проекте

Я уже упомянул, что количество цветов LEGO ограничено. Их что-то около 40 совершенно разнообразных. На проекте используется библиотека Pillow. Через которую практически всё делается с помощью одной строчки кода. Так к изображению применяется изменение размера, color, sharpness, contrast, brightness. Hue и saturation и прочие свистелки-перделки. В конечном итоге полностью заменяется палитра.

Помимо этого, конечно, происходит конвертация изображений к нужным форматам. Например, перегоняется из P в RGBA, потом убирается альфа и остается RGB. На выходе имеем изображением с 8-битным цветом. Следовательно, нам нужна палитра из 256 цветов, а мы помним, что уникальных цветов у нас ~40. Что делать? Нуу, например, удлинить палитру просто продолжив последовательность, пока не наберем нужное количество! Честно, я не могу это объяснить, данный код был написан прошлым разработчиком из известной в Беларуси, да и не только, компании) Передаю привет Игорю.

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

А дальше дело за подкапотной Pillow! Метод .convert() вызывается у исходного изображения, в котором надо изменить палитру. Принимает в аргументах режим (мод), в нашем случае это 8bit – мод P, алгоритм дизеринга (None – не применять) и наше изображение с палитрой. Вот так просто.

В документации расписали какой и когда алгоритм используется – PIL.Image.Image.convert

Результат пикселизации

Ниже исходное изображение и результаты в размере 512x512 и 128x128.

Генерация превью

Дело осталось за малым – рендер. У нас есть изображение достаточно маленьких размеров, например, 256 на 256 пикселей. Оно, в свою очередь, в PDF будет состоять из тайлов, например, 32x32. Для рендера превью просто проходимся по каждому пикселю изображения, маппим цвет пикселя с картиной блока LEGO и собираем так целое изображение, состоящее из 256 изображений блоков.

Генерация PDF

В самом начале я говорил, что генерация PDF уже была, но, увы, та библиотека, на которой оно работало, мне не подходила. От меня просили векторную графику, чтобы при увеличении не терялось качество, но составлять надо было схемы, конечно же, из иконок, которые обозначают цвет. Поэтому я начал копать в сторону использования SVG в библиотеке reportlab. С помощью неё и сгенерировал всё. Помимо векторной графики необходима поддержка разных размеров и форматов. Так, например, PDF должна быть в формате А3 и А4, цветном и чёрно-белом режиме. В чёрно-белом присутствовали только изображения (иконки) цветов, а в цветном у них был фон. Ко всему этому мы имеем кучу размеров картин, количества тайлов и их расположения! Всё это должно было быть мега резиновым! Таким и вышло. Практически все размеры высчитываются динамически при генерации, где-то используются коэффициенты. Код от такого преображается в худшую сторону. Везде сплошные подсчеты отступов, ширин и высот, координат размещения, границ объектов.

Первая страница содержит превью картины и правильное расположение тайлов для сборки. Дальше идет N количество страниц, равное количеству тайлов. Под каждой таблицей есть количество необходимых LEGO-блоков, а на первой странице общее количество.

Примеры сетки:

Как вы понимаете, количество комбинацией куча. Так что главная задача – сделать масштабируемо!

Вот так выглядит схема одного тайла в цветном и чёрно-белом варианте варианте:

Не спрашивайте почему в таблице-статистике остались цвета, хотя PDF в черно-белом формате рассчитана для пользователей, у которых нет цветного принтера. Таково пожелание заказчика. Я вообще не понимаю, почему они сами распечатать не могут и положить в коробку вместе с LEGO, но это не моего ума дело.

Вместо выводов и итогов

Считаю, что получилось хорошо, но можно было лучше. Мне не совсем нравятся границы таблицы, выравнивание в таблице-статистике. Но я шатал эти размеры шрифтов высчитывать.

А ТЕПЕРЬ О САМОМ ВКУСНОМ, НО В ХАОТИЧНОМ ФОРМАТЕ.

Помните, я говорил про вектор? Дык я сделал, да, без проблем. Нашел библиотеку, что конверит svg в rlg (reportlab graphics object). Так и называется, svg2rlg. И всё бы ничего, но генерация на моем компьютере занимала больше минуты. Под две даже. ДВЕ минуты на генерацию PDF, карл! Я пытался оптимизировать, конечно же только один раз прогревал эти svg файлы конвертя в rlg, пытался оптимайзить, но узкое место и было как раз в использование этих объектов.

Как бы уже все смирились с тем, что столько будет генериться… Покажут проверку оплаты пользователю и ладно. Дошло дело до деплоя… Вся эта радость оказалась не на машине нормальной, человеческой, хотя тут FastAPI со всеми эндпоинтами, а, блин, на AWS лямбдах! И вызов функций через gateway! В обход всей валидации параметров, что дает FastAPI! Ну это ничего, подумал я, но стало чего тогда, когда я узнал, что gateway не держит timeout больше 30 секунд! Даже за тысячу долларов тебе не включат такую возможность. Пошел я сообщать печальную новость и предложил три варианта решения проблемы.

Я не бросил экспериментировать с оптимизацией и попробовал поработать с PNG и JPG. Вариант с PNG 1000x1000 не лучше SVG, а даже хуже, а вот с JPG отрабатывает за считанные секунды! Я был удивлен скорости! Генерация PDF с 2 минут упала до 3-4 секунд! Вы представляете? Я радуюсь тому, что запрос работает за секунды, когда мы все привыкли к мс! Только за счет того, что я не использовал png/svg, а взял jpg! Объяснил ситуацию, взяли 1000x1000 изображения, что дают хороший запас увеличения PDF без потери качества, запросил изображения с цветным фоном, так как это jpg и я не могу отрисовать фон сам в ячейке.

Короче, я сдал проект.

Недавние посты


31.12.2023 Итоги Года

Итоги Года 2023

© marshal.by 2023

Исходный код

Сайт работает на Gatsby + prismic и опубликован на GitHub.