Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Все любят генеративное искусство и всё что с ним связано (вот оно слева направо, в конце есть прикольные ссылочки).
(p.s. в описаниях к сгенерированным изображениям фразы, которые я подавал на в генератор)
Мне тоже было интересно копнуть в эту сторону, и недавно мне попал в руки сайт, который генерирует изображения по фразе. Также на сайте было сказано, что исходники закрыты, но есть пример подобной генерации на Python. Так как я питонист и интересна тема, то решил разобраться в работе алгоритма.
Скрипт генерации представлял из себя один файл, который через Tkinter случайным образом создавал картинку. Так как лучший способ полностью понять работы кода - это зарефакторить его, то этим я и занялся, и вот ссылка на перерефакторенную версию скрипта.
А теперь немного к сути работы.
Общее описание
Random Image Generator - это небольшая программа, которая генерирует изображение по заданной фразе. Пример:
Общая суть генерации заключается в том, что создаётся некий арт, и затем он применяется ко всем пикселям изображения. По сути, арт представляет собой огромную формулу из операторов с большой вложенностью. Оператор - это отдельный математический оператор (логично), например, синус/косинус/среднее, который принимает одни флоатики и выдаёт другие флоатики.
Флоатики, которые преобразуются в цвет, изначально берутся из положения пикселя отностительно центра изображения. Это позволяет получать одинаковые результаты при каждой генерации для одного арта и бесконечно масштабировать картинку. Положение подаётся как position = (size / pixel_number) * 2 - 1
чтобы иметь значение в [-1; 1]. А при установке пикселя на изображение каждый канал из float(-1; 1) переводится в int(0; 255).
Арт
Так как формула хранится в арте, а затем лишь вычисляется для пикселей, то и создание итогового узора заложено именно там. И арт как раз есть то, что отвечает за красивые цвета и нелинейность получаемого изображения. Так что для понимания работы скрипта нужно понять, что происходит внутри арта и при его генерации.
Вот его пример, каждое название - отдельный оператор:
Длинное
Mod(
Tent(
Product(
VariableY(),
Sum(
Constant(
value=(0.3244364873390073, 0.7708296465915099, 0.9332060500466999)
),
Constant(
value=(0.5041825115944022, 0.6632634769751835, 0.1613102126504703)
)
)
)
),
Tent(
Tent(
Sin(
Mix(
Product(
Sin(
Mix(
Mix(
Level(
VariableY(),
VariableX(),
VariableY(),
treshold=-0.10683892347003532
),
Tent(
VariableY()
),
Sum(
Sum(
Level(
VariableX(),
Constant(
value=(0.675473655969658, 0.5954164416187114, 0.449629381492357)
),
VariableY(),
treshold=-0.5928609645964091
),
VariableY()
),
VariableY()
)
),
Well(
Constant(
value=(0.5503713883487658, 0.49962352442393165, 0.7688050540403824)
)
),
Level(
Sin(
Constant(
value=(0.8225282623320913, 0.3883003304362743, 0.9568771917767398)
),
phase=2.0376783801515193,
frequency=3.9896008410954966
),
Mix(
VariableX(),
VariableX(),
VariableY()
),
VariableY(),
treshold=-0.621706133226186
)
),
phase=2.6735684842393255,
frequency=1.3136102663251883
),
Tent(
Mix(
VariableY(),
Product(
Sum(
Level(
VariableY(),
Sin(
VariableY(),
phase=3.004718155488889,
frequency=1.4456333501008813
),
Mix(
VariableX(),
Constant(
value=(0.9345878558040448, 0.20889624624509862, 0.6315200232850579)
),
Product(
Constant(
value=(0.5330792534910264, 0.7945688505346382, 0.47051673946382233)
),
VariableY()
)
),
treshold=-0.38212437520625087
),
VariableY()
),
Well(
Mod(
Constant(
value=(0.400829150439062, 0.3021058233122762, 0.598367884016014)
),
Sum(
VariableY(),
Constant(
value=(0.24750821288915192, 0.5625010703560506, 0.21725209844356919)
)
)
)
)
),
Level(
Constant( ## (1)
value=(0.8052143634939728, 0.8271696932063766, 0.6657108279633096)
),
Sum(
Mod(
Constant(
value=(0.9054037881002341, 0.12796220865865926, 0.4943414103445982)
),
Mod(
Product(
VariableX(),
Tent(
VariableX()
)
),
VariableX()
)
),
Product(
Constant(
value=(0.2665617642620427, 0.14347704782011006, 0.5622638203078165)
),
Level(
Constant(
value=(0.23086906415935038, 0.37527352432134564, 0.6550565938107306)
),
Constant(
value=(0.9743539853372215, 0.4993488372065832, 0.05428706152991847)
),
VariableX(),
treshold=0.7809432727354246
)
)
),
Product(
VariableX(),
Constant(
value=(0.35945935485851765, 0.4090601858176649, 0.7061945311933203)
)
),
treshold=0.5365108582149631
)
)
)
),
Product(
Sin(
VariableY(),
phase=1.3028027533404898,
frequency=3.4563327126372814
),
Constant(
value=(0.7204939582620923, 0.11638980673023169, 0.06796149180653843)
)
),
Well(
Mod(
Level(
Mod(
Constant(
value=(0.8232688016240477, 0.7483540167019266, 0.17127382751327436)
),
VariableX()
),
Constant(
value=(0.8002626489803971, 0.2922622157788455, 0.2775479167197744)
),
Product(
VariableY(),
VariableX()
),
treshold=-0.544640789851953
),
VariableY()
)
)
),
phase=1.9838654684998096, ## (2)
frequency=4.625078885960704
)
)
)
)
Видно, что он длинный и имеет большую вложенность, хотя сложность
(о ней чуть ниже) генерации не сильно большая. Если же поменять какое-то значение, то изображение должно поменяться. Вот примеры изображений, в которых (1) обнулён оператор Constant(value=(0.8052, 0.8272, 0.6657))
на Constant(value=(0.0, 0.0, 0.0))
и в метке (2) phase=1.9839, frequency=4.625
изменено на phase=1.0, frequency=3.0
.
Можно посмотреть, что будет, если изменить сами операторы. Первую метку заменим Constant
-> VariableX
, а во второй Sin
-> Well
. Вот что получается:
В первом случае, так же как и при изменении значений, изменения на детализации, а во втором изменяется общий узор.
Общее сравнение полученных изображений
Подробнее разберём генерацию артов. Вот пример сгенерированного арта маленькой сложности:
Well(
Mod(
Level(
VariableX(),
Product(
Sum(
VariableY(),
VariableY()
),
VariableX()
),
VariableY(),
treshold=-0.36504581083005916
),
Sum(
Sum(
Tent(
Sum(
Product(
VariableX(),
Product(
VariableX(),
VariableY()
)
),
Constant(
value=(0.8837650122639356, 0.33526228359302135, 0.09636463380778282)
)
)
),
Well(
VariableX()
)
),
VariableX()
)
)
)
Дерево операторов в более наглядном виде:
Для генерации арта необходим параметр сложность
, и это как раз то, насколько глубока будет генерация дерева. Для маленькой сложности дерево будет не особо большим (в этом примере сложность 12), но чем больше она будет - тем больше арт (в примере выше было 48) и сложнее изображение. Число сложности не имеет под собой каких-то особых вычислений, это просто случайно натыканный инт.
Алгоритм создания арта (дерева) просто рекурсивно создёт операторы, каждый раз уменьшая сложность у ветвей. Весь код генерации арта:
def generate_art(complexity: int) -> Operator:
if complexity <= 0:
# operators_flat - операторы с арностью 0
plain_operator = random.choice(operators_flat)
return plain_operator()
# operators_dimensional - операторы с арностью > 0
operator = random.choice(operators_dimensional)
sub_complexities = [
random.randrange(complexity)
for _ in range(operator.arity - 1)
]
suboperators = []
last_complexity = 0
for curr_complexity in sorted(sub_complexities):
suboperator = generate_art(curr_complexity - last_complexity)
suboperators.append(suboperator)
last_complexity = curr_complexity
suboperators.append(generate_art(complexity - 1 - last_complexity))
return operator(*suboperators)
В этом коде есть два списка - operators_flat
и operators_dimensional
. Это два списка операторов, разница между которыми в их арности: в первом она нулевая, во втором ненулевая. Ключевая разница операторов в этих списках состоит в том, что ненулевые операторы имеют дочерних (по количеству арности), а нулевые не имеют. Нулевые просто выбирают значение, по которому будет происходить генерация (координата x/y/const), но не имеют никаких формул внутри, и ими же заканчивается любая ветвь в дереве арта. А ненулевые получают значения из дочерних, а дальше изменяют то, что получили, по описанной в них формуле. Так, единичные меняют одно значение по математической формуле, операторы с арностью 2+ же смешивают значение из нескольких цветов.
Входные параметры
Результат генерации арта зависит от 2-х вещей: состояние рандома и сложность. Состояние рандома может (и так делалось изначально) браться случайным, но его можно и устанавливать вручную перед генерацией, чтобы получать одинаковые результаты. Передо мной стояла задача генерировать изображения из фраз, поэтому за семя я беру фразу и устанавливаю её рандому непосредственно перед генерацией арта.
Сложность тоже нужно откуда-то брать при генерации, но в то же время необходимо иметь возможность задавать её вручную. Поэтому, если сложность не задана, то перед генерацией тем же ядром получаем случайное число из некоторого промежутка, которое затем используется как сложность.
Вот как изменяются картинки с одной фразой, но разной сложностью. Это интересно тем, как постепенно изменяется изображение с увеличением сложности арта. Здесь фраза случайная, а сложность изменяется от 1 до 148.
Картинки-картинки-картинки
А вот много изображений, не какие-то определённые результаты, а просто генерация по строчкам песни подряд (кстати, получилось удачно, как по мне).
Кто знает песню - молодец
Дальше
Что будет с этим всем дальше? Скорее всего, я немного потыкаю код и забью, это все мы любим делать, но в теории можно добавить:
новые операторы, операторы 4+ арности: сейчас операторов не так много, и интересно бы было добавить много новых, чтобы было меньше повторяющихся шаблонов, больше вариантов
компиляцию, кэширование вычислений: вычисление арта представляет их себя числодробление флоатов много раз для каждого пикселя. Питон, на котором написан генератор, в этом плох, но компилирование может сильно ускорить работу
гуишку: запуск идёт на чёрном фоне из консоли, а можно красиво и приятно для юзеров-непрограммистов запускать генерацию, более удобно передавать арт
Ссылочки
Не знаю, о чём ещё тут писать, поэтому держите красивые ссылочки:
https://www.shvembldr.com/gallery/
https://www.behance.net/manoloide
https://tylerxhobbs.com/work
https://generated.space/
https://sunandstuff.com/mandelbrot/about/
http://ravenkwok.com/
http://reas.com/
https://inconvergent.net/