Начну с оффтопа

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

Здесь помочь может дизайнер, который вставит изображение на подходящий фон, или просто при загрузке выбирать, какую часть изображения придется отрезать. Оба варианта по своим причинам неудобны. Первый — из-за времени, второй — из-за потери содержания. Отсюда и родилась задача написать сервис, который автоматически будет выполнять работу дизайнера: возьмет изображение и поместит его в рамки с необходимым соотношением сторон. Получится примерно следующее:

Оригинал фото взять из Интернета. Белые рамки наложены написанным мной сервисом
Оригинал фото взять из Интернета. Белые рамки наложены написанным мной сервисом

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

Дисклеймер

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

Методика построения градиента

Поскольку цветовые компоненты изображения представляют из себя числа, например RGB {173, 217, 54}, то при построении градиента мы имеем переход от одного числа до другого. Здесь сразу вспоминаются школьные арифметическая и геометрическая прогрессии, которые помогут нам в расчетах.

Линейный градиент

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

a_{n+1} = a_{n} + d

Здесь d и есть разность арифметической прогремсии. Из этой формулы можно вычислить следующие параметры:

a_{n} = a_{0} + nda_{n} = a_{1} + (n-1)dd = \frac{a_{n} - a_{0}}{n}=\frac{a_{n} - a_{1}}{n-1}

Именно последняя формула нам и даст необходимый шаг градиента. Под шагом градиента я понимаю то, на сколько цветовая компонента пикселя (n+1) отличается от пикселя (n). Условно, параметр R изменился с 201 до 203. Здесь шагом является 201-201=2.

Но какой подход использовать дальше? Из уравнения d = \frac{a_{n} - a_{0}}{n}следует, что d существует при любом n > 0. Подразумевается, что градиент строится между несколькими пикселями, поэтому условие выполняется всегда.

Далее я предполагаю высчитывать параметр d_{c} для каждой компоненты пикселя. В моем случае, в формате RGB будут существовать d_{R}, d_{G} и d_{B}. В первом предположении, которое я пока что не тестировал, изменяться они будут независимо друг от друга.

Математика будет следующей: берем у исходного изображения в верхней строке цвет 1-го и последнего пикселей. Вычисляем для каждого из них d = \frac{a_{n} - a_{1}}{n-1}. В общем случае оно будет представлять вещественное число с плавающей точкой. Представляем каждую компоненту цвета аналогично в виде числа с плавающей точкой и каждый последующий пиксель изменяем по формуле a_{n+1} = a_{n} + d.

Для отображения же конкретного цвета я предполагаю отсекать дробную часть или, по-другому, округлять a_{n+1} в меньшую сторону. За счет этого создастся плавный переход от начального цвета к конечному.

Послесловие

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

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

Комментарии (4)


  1. ParaMara
    09.10.2024 20:35

    Я бы делал совсем иначе - первый и последний пиксель есть случайные величины.

    Усреднить цвет по границе изображения на десяток пикселей - человек так видит. Сгладить - получится линия вдоль границы. Расширить её на всю добавляемую полоску - это можно сделать разными способами. Например.

    Просто повторить пока полоса не заполнится. Повторять и каждый раз немного сглаживать - изображение будет как-бы выступать.. Переносить цвет по прямым из центра изображения. И так далее…


  1. Serge3leo
    09.10.2024 20:35

    Не понятно, картинка с уже предложенным Вами градиентом? Похоже, что нет. Попробуйте построить четыре тестовые картинки: 100 пикселов сверху, слева, справа и снизу.

    Броюсь что проблема не в том, где лучше делать линейный градиент, в пространстве RGB, или в пространстве HSV, а в самой формулировке идеи дополнения.


  1. plFlok
    09.10.2024 20:35

    например RGB {173, 217, 54}

    Правильно ли я понимаю, что вы предлагаете линейно интерполировать промежуточные пиксели для каждого из каналов RGB для достижения градиента?

    В таком случае не рекомендую этого делать именно в RGB кодах, так как с большой степенью вероятности в середине градиента будут "средние" по яркости пиксели со значением близким к 128 из 255 в каждом из каналов. Картинка будет отдавать серым грязным оттенком в середине.

    Рекомендую перейти к цветовым пространствам, которые описывают восприятие цвета глазом, а не кодирование его для железки. Например, HSL, и сделать интерполирование там. А потом уже можно вернуть обратно в RGB.

    Пример градиентов от чисто красного 255, 00, 00 к чисто зелёному 00, 255, 00.
    RGB получает в середине 88, 88, 00 что является "оранжеватым" оттенком серого - яркость красных пикселей упала в 2 раза, яркость зелёных - только на половине своей мощности.

    HSL получает в переводе на RGB в середине 00, 00, 255 - что является жёлтым - цветом посередине между красным и зелёным в восприятии цвета глазом, причём яркость цвета сохраняется.

    hsl
    hsl
    rgb
    rgb

    UPD: Хотя пространство HSL закольцовано, там может быть 2 градиента - в зависимости от того, в каком направлении по кругу идти. может быть ещё такой вариант - 00,00,255 в середине вместо 255,255,0

    Становится много картинок, скрою под спойлер
    hsl, но длинным путём
    hsl, но длинным путём

    UPD2: рассмотрите использование библиотеки imagemagic, она повсеместно распространена, портирована на множество языков и даже встроена в php как часть языка. Там все эти проблемы цветовых пространств и красивых градиентов уже решены, не нужно писать свой код.


  1. ahabreader
    09.10.2024 20:35

    Если рассматривать это как реальную задачу ("написать сервис"), а не упражнение ("как математически просчитать"), то выглядит страшно, очень страшно. Я бы упростил до:

    Растровая картинка с линейным градиентом по двум точкам - это картинка 2x1 пикселя, увеличенная с билинейной фильтрацией (до нужного размера - 1280x853). Ну, нам надо две полосы, можно взять картинку 2x2.

    Цвет этих 4 пикселей? Использовать усреднённые цвета исходной картинки по углам. Уменьшить исходник где-нибудь до 5x5[1][2] и взять угловые пиксели.

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

    Чтобы градиент не распадался на полосы, лучше работать в 10+ битах и дизерить при понижении разрядности до 8 бит.

    В чём это делать? Взять какую-нибудь высокоуровневую библиотеку для работы с картинками, чтобы из коробки всё было доступно: ресайз с фильтрами на выбор, несколько видов дизеринга плюс интерфейс почеловечнее, чем у imagemagick. Imagemagick можно использовать как Arch Linux не трогать, но пользоваться его документацией с хорошей теорией.

    [1] снова можно выбрать билинейную фильтрацию, теперь ради скорости

    [2] лучше выглядит, если использовать размер, обратно пропорциональный ширине полос (5x5 для широких полос, 10x10 для узких, условно говоря)