Мы уже писали о поле течений, при помощи которого можно генеративно рисовать невероятно красивые, завораживающие линии. Сегодня, к старту курса о Fullstack-разработке на Python, мы решили продолжить тему. Представляем перевод статьи, автор которой рассказывает, как генерировать рисунки деревьев при помощи L-систем, которые состоят из алфавита и могут применяться для создания порождающих правил подстановки.
Креативный кодинг — это весело, если вы знаете кое-что о программировании. И если, как и я, вы — полный нуб, когда дело доходит до кистей и карандашей, это ещё более заманчивое средство самовыражения и создания чего-либо без особой цели. Оно не обязательно должно быть функциональным, но на этом пути вы можете оттачивать свои хакерские навыки и ставить перед собой сложные задачи.
Мрй коллега, дата-сайентист из Art & AI, недавно указал мне на область генеративного искусства. Меня всегда восхищала эмерджентность, и это чувство даже усилилось, когда я наткнулся на приведённое выше изображение. В этом посте рассказывается о том, как нарисовать лес так, чтобы рисунок был похож на карандашный набросок, с помощью языка Processing. Слово за слово: я энергично взялся за язык Processing в режиме Python, поверхностно изучал matplotlib для анимации и читал книгу Шиффмана the Nature of Code.
«Можно ли создать на Python проект, который способен генерировать такие карандашные рисунки случайных лесов с деревьями различных видов?», — подумал я (простите за каламбур).
Конечно, кто-то уже думал об этом и даже придумал название: алгоритмическая ботаника. Пшемыслав Прусинкевич и Аристид Линденмайер в 1990 году написали на эту тему основополагающую книгу Algorithmic Beauty of Plants. Линденмайер изобрёл L-системы ещё в 1968 году. Сегодня у домашних компьютеров достаточно ресурсов для работы с большими параллельными системами записи, и вы можете приступить к моделированию роста любого растения, используя такую платформу, как L-Py.
Хотя на эту тему написано много программ и существует довольно много библиотек с открытым исходным кодом, это не совсем то, что я имею в виду. Вместо этого возникла идея создания предметов генеративного искусства на языке Python с использованием следующих компонентов:
AXIOM = 'A'
RULES = {'A': 'AB',
'B': 'A'}
# initialize
productions = [AXIOM]
iterations = 7
def generate_d0l(axiom, productions, iterations):
def rewrite_l(word):
productions.append(''.join([RULES.get(c) or c for c in word]))
return productions[-1]
if iterations == 0:
return axiom
else:
return rewrite_l(generate_d0l(axiom, productions, iterations - 1))
# main
generate_d0l(AXIOM, productions, iterations)
print(''.join([f"{i}: {p}\n" for i,p in enumerate(productions)]))
Результат
На этом ознакомительном сайте о рекурсии я нашёл рекурсивную реализацию на Python. Хотя я всё ещё изо всех сил пытаюсь применить рекурсию, я нахожу удовлетворительным решение проблем кодирования таким образом. И даже с этими 20 строками кода мы уже наткнулись на числа Фибоначчи! Взгляните на длину каждой последовательности:
[1, 2, 3, 5, 8, 13, 21, 34]
Какой бы удивительной ни была природа кода, мы по-прежнему далеки от реального генерирования дерева. Для этого нам нужны правила ветвления. Чтобы установить их, базовые L-системы также допускают константы, которые не затрагиваются порождающими правилами. Обычно добавляют константы + и - для обозначения поворотов при ветвлении и [,] для обработки расположения точек ветвления. Код фрактального растения выглядит следующим образом:
AXIOM = "X"
RULES = {"X": "F+[[X]-X]-F[-FX]+X",
"F": "FF"}
productions = [AXIOM]
generate_d0l(AXIOM, productions, iterations)
print(''.join([f"{i}: {p}\n" for i,p in enumerate(productions)]))
Результат
Удивительно видеть, как при добавлении несколько более длинных правил возрастает сложность. Чтобы действительно что-то нарисовать, мы переводим буквы следующим образом:
p1: F(0.33) → F[+F]F[−F]F
p2: F(0.33) → F[+F]F
p3: F(0.34) → F[−F]F
В этом случае реализация Python могла бы выглядеть примерно так:
from numpy.random import default_rng
AXIOM = "F"
RULES = {"F": ["F[+F]F[−F]F", "F[+F]F", "F[−F]F"]}
productions = [AXIOM]
iterations = 3
rng = default_rng()
def generate_stochastic(axiom, productions, iterations):
def get_stochastic_rule(letter):
if letter not in RULES.keys():
return letter
else:
return RULES.get(letter)[rng.choice(3,1)[0]]
def rewrite_l(word):
productions.append(''.join([get_stochastic_rule(c) for c in word]))
return productions[-1]
if iterations == 0:
return axiom
else:
return rewrite_l(generate_stochastic(axiom, productions, iterations - 1))
generate_stochastic(AXIOM, productions, iterations)
print(''.join([f"{i}: {p}\n" for i,p in enumerate(productions)]))
Результат
Вот ещё один прогон с другим результатом:
Результат
from collections import namedtuple
# parameters
F = namedtuple('F', ['s', 't', 'o']) # parameterized cell
s = {2: "AS", 1: "BS"} # cell size
t = {1: "A", 2: "B"} # cell type
o = {1: "right", -1: "left"} # cell orientation
AXIOM = [F(1, 2, 1)]
RULES = {F(2,1,1): [F(2,1,-1), F(1,2,1)],
F(2,1,-1): [F(1,2,-1), F(2,1,1)],
F(1,2,1): [F(2,1,1)],
F(1,2,-1): [F(2,1,-1)],
}
productions = [AXIOM]
iterations = 5
def unnest(list_, unnested_=None):
"""Utility function to unnest arbitrarily deep list of lists
"""
if unnested_ is None:
unnested_ = []
for x in list_:
if not isinstance(x, list):
unnested_.append(x)
if isinstance(x, list):
unnest(x, unnested_)
return unnested_
def generate_parametric(axiom, productions, iterations):
def rewrite_l(word):
productions.append(unnest([RULES.get(c) or c for c in unnest(word)]))
return productions[-1]
if iterations == 0:
return [axiom]
else:
return rewrite_l(generate_parametric(axiom, productions, iterations - 1))
generate_parametric(AXIOM, productions, iterations)
print(''.join([f"{i}: {p}\n" for i,p in enumerate(productions)]))
Результат
В то время как в приведённом выше примере Анабены делаются только простые сравнения, в параметрических L-системах возможно любое выражение, использующее параметры в сочетании с арифметическими операторами +, −, ∗, /; оператором возведения в степень ∧, операторами отношения <, >, =; логическими операторами !, &, | (не, и, или) и т. д. Например, мы могли бы определить параметр time, значение которого увеличивается с каждой итерацией. Это можно использовать для увеличения ширины ветвей дерева.
В примерах из книги «The Nature of Code» фактически это было реализовано на языке Processing. Однако, вместо того чтобы ограничивать себя настольным компьютером, можно увеличить пробег, используя библиотеку p5js, которая представляет собой «...интерпретацию Processing для сегодняшнего Интернета». Чтобы продемонстрировать это, возьмём следующий пример:
AXIOM = 'G'
RULES = {
'F': 'FF',
'G': 'F-[[G]+G]+F[+FG]-G'
}
productions = [AXIOM]
generate_d0l(AXIOM, productions, iterations=5)
print(''.join([f"{i}: {p}\n" for i,p in enumerate(productions)]))
Результат
Саммер Ризо уже реализовал L-системы в библиотеке p5js, которая доступна на сайте thefractal.zone. Копирование аксиомы и правил, а также установка угла ветвления θ=30° сгенерируют приведённый ниже рисунок. Обратите внимание, что в коде показаны только 5 итераций, в то время как рисунок основан на большем количестве итераций (я поставил ползунок на максимум). При использовании рекурсии в Python можно быстро достигнуть предела: когда я пытался выполнить iterations=10 на стандартной виртуальной машине, блокнот Deepnote выдал ошибку IOPub data rate exceeded. Ещё одна проблема — чтение и применение оптимизации хвостовых вызовов в Python.
Рассмотрев алгоритмические и технические компоненты, мы можем собрать всё воедино. Есть ещё довольно много вещей, с которыми можно поэкспериментировать, чтобы создавать реалистичные эскизы, такие как добавление шума для рисования «схематичных» линий, увеличение толщины ветвей, добавление теней с засветкой и создание сцены с наложением деревьев.
Как питонист я был рад найти библиотеку pyp5js, которая позволяет повторно использовать идеи Processing в Python и перекодировать их в p5.js с помощью Transcrypt.
Такой подход охватывает все методы из p5.js и даёт нам достаточно инструментов для рисования, которыми можно поиграть. Но это предназначено для другой публикации. А пока я оставляю вас с генеративным творением Андерса Хоффа, которое может вдохновить вас на создание предметов генеративного искусства на Python.
Разработанный 27 лет назад Python 1.0 сегодня — не просто зрелый язык программирования, но язык в расцвете сил: у него есть не только развитая экосистема, но и философия, направленная на простоту и ясность программирования, а значит, расцвет языка будет продолжаться ещё долго.
Вместе с тем огромными темпами развивается веб: от веб-страниц мы пришли к веб-приложениям, иными словами браузер стал операционной системой в миниатюре. Если вам интересно поработать на стыке этих областей, вы можете присмотреться к курсу Fullstack-разработчик на Python, где студенты получают системную подготовку, приобретая все знания, необходимые для старта карьеры.
Источник статьи: https://habr.com/ru/company/skillfactory/blog/559390/
Креативный кодинг — это весело, если вы знаете кое-что о программировании. И если, как и я, вы — полный нуб, когда дело доходит до кистей и карандашей, это ещё более заманчивое средство самовыражения и создания чего-либо без особой цели. Оно не обязательно должно быть функциональным, но на этом пути вы можете оттачивать свои хакерские навыки и ставить перед собой сложные задачи.
Мрй коллега, дата-сайентист из Art & AI, недавно указал мне на область генеративного искусства. Меня всегда восхищала эмерджентность, и это чувство даже усилилось, когда я наткнулся на приведённое выше изображение. В этом посте рассказывается о том, как нарисовать лес так, чтобы рисунок был похож на карандашный набросок, с помощью языка Processing. Слово за слово: я энергично взялся за язык Processing в режиме Python, поверхностно изучал matplotlib для анимации и читал книгу Шиффмана the Nature of Code.
«Можно ли создать на Python проект, который способен генерировать такие карандашные рисунки случайных лесов с деревьями различных видов?», — подумал я (простите за каламбур).
Конечно, кто-то уже думал об этом и даже придумал название: алгоритмическая ботаника. Пшемыслав Прусинкевич и Аристид Линденмайер в 1990 году написали на эту тему основополагающую книгу Algorithmic Beauty of Plants. Линденмайер изобрёл L-системы ещё в 1968 году. Сегодня у домашних компьютеров достаточно ресурсов для работы с большими параллельными системами записи, и вы можете приступить к моделированию роста любого растения, используя такую платформу, как L-Py.
Хотя на эту тему написано много программ и существует довольно много библиотек с открытым исходным кодом, это не совсем то, что я имею в виду. Вместо этого возникла идея создания предметов генеративного искусства на языке Python с использованием следующих компонентов:
- облегчённая реализация L-систем, которая также поддерживает стохастические и параметрические порождающие правила;
- интеграция с p5js посредством pyp5js в качестве нативного для веба графического движка.
Играем с L-системами на языке Python
Существуют L-систем различных видов возрастающей сложности. Начнём с самой простой: оригинальная L-система Линденмайера для моделирования роста водорослей (мы сразу перейдём к ней, прочитайте статью в Википедии, если нужен более подробный контекст).D0L: детерминированная контекстно-свободная L-система
# define L-systemAXIOM = 'A'
RULES = {'A': 'AB',
'B': 'A'}
# initialize
productions = [AXIOM]
iterations = 7
def generate_d0l(axiom, productions, iterations):
def rewrite_l(word):
productions.append(''.join([RULES.get(c) or c for c in word]))
return productions[-1]
if iterations == 0:
return axiom
else:
return rewrite_l(generate_d0l(axiom, productions, iterations - 1))
# main
generate_d0l(AXIOM, productions, iterations)
print(''.join([f"{i}: {p}\n" for i,p in enumerate(productions)]))
Результат
На этом ознакомительном сайте о рекурсии я нашёл рекурсивную реализацию на Python. Хотя я всё ещё изо всех сил пытаюсь применить рекурсию, я нахожу удовлетворительным решение проблем кодирования таким образом. И даже с этими 20 строками кода мы уже наткнулись на числа Фибоначчи! Взгляните на длину каждой последовательности:
[1, 2, 3, 5, 8, 13, 21, 34]
Какой бы удивительной ни была природа кода, мы по-прежнему далеки от реального генерирования дерева. Для этого нам нужны правила ветвления. Чтобы установить их, базовые L-системы также допускают константы, которые не затрагиваются порождающими правилами. Обычно добавляют константы + и - для обозначения поворотов при ветвлении и [,] для обработки расположения точек ветвления. Код фрактального растения выглядит следующим образом:
AXIOM = "X"
RULES = {"X": "F+[[X]-X]-F[-FX]+X",
"F": "FF"}
productions = [AXIOM]
generate_d0l(AXIOM, productions, iterations)
print(''.join([f"{i}: {p}\n" for i,p in enumerate(productions)]))
Результат
Удивительно видеть, как при добавлении несколько более длинных правил возрастает сложность. Чтобы действительно что-то нарисовать, мы переводим буквы следующим образом:
- «F» означает «потянуть [линию рисования] вперёд»;
- «-» означает «повернуть вправо», используя предварительно определённое значение, например 30°;
- «+» означает «повернуть влево на 30°»;
- «X» не соответствует какому-либо действию рисования и используется для управления эволюцией кривой.
Добавление стохастических правил
Системы D0L полностью детерминированы, то есть каждая исходная аксиома даёт один и тот же результат. Применение этого способа рисования леса ведёт к скучной сцене: все деревья будут одинаковыми. Чтобы имитировать природную изменчивость, стохастические L-системы позволяют использовать одну и ту же букву для разных порождающих правил с заданной вероятностью. Например, нам нужны для F три разных порождающих правила с равной вероятностью:p1: F(0.33) → F[+F]F[−F]F
p2: F(0.33) → F[+F]F
p3: F(0.34) → F[−F]F
В этом случае реализация Python могла бы выглядеть примерно так:
from numpy.random import default_rng
AXIOM = "F"
RULES = {"F": ["F[+F]F[−F]F", "F[+F]F", "F[−F]F"]}
productions = [AXIOM]
iterations = 3
rng = default_rng()
def generate_stochastic(axiom, productions, iterations):
def get_stochastic_rule(letter):
if letter not in RULES.keys():
return letter
else:
return RULES.get(letter)[rng.choice(3,1)[0]]
def rewrite_l(word):
productions.append(''.join([get_stochastic_rule(c) for c in word]))
return productions[-1]
if iterations == 0:
return axiom
else:
return rewrite_l(generate_stochastic(axiom, productions, iterations - 1))
generate_stochastic(AXIOM, productions, iterations)
print(''.join([f"{i}: {p}\n" for i,p in enumerate(productions)]))
Результат
Вот ещё один прогон с другим результатом:
Результат
Добавление параметров
В параметрической L-системе буквам разрешается иметь параметры, соответствующие используемым в спецификации порождающих правил L-систем формальным параметрам. С помощью этого подхода можно смоделировать Anabaena catenula (взято из диссертации Ханана):from collections import namedtuple
# parameters
F = namedtuple('F', ['s', 't', 'o']) # parameterized cell
s = {2: "AS", 1: "BS"} # cell size
t = {1: "A", 2: "B"} # cell type
o = {1: "right", -1: "left"} # cell orientation
AXIOM = [F(1, 2, 1)]
RULES = {F(2,1,1): [F(2,1,-1), F(1,2,1)],
F(2,1,-1): [F(1,2,-1), F(2,1,1)],
F(1,2,1): [F(2,1,1)],
F(1,2,-1): [F(2,1,-1)],
}
productions = [AXIOM]
iterations = 5
def unnest(list_, unnested_=None):
"""Utility function to unnest arbitrarily deep list of lists
"""
if unnested_ is None:
unnested_ = []
for x in list_:
if not isinstance(x, list):
unnested_.append(x)
if isinstance(x, list):
unnest(x, unnested_)
return unnested_
def generate_parametric(axiom, productions, iterations):
def rewrite_l(word):
productions.append(unnest([RULES.get(c) or c for c in unnest(word)]))
return productions[-1]
if iterations == 0:
return [axiom]
else:
return rewrite_l(generate_parametric(axiom, productions, iterations - 1))
generate_parametric(AXIOM, productions, iterations)
print(''.join([f"{i}: {p}\n" for i,p in enumerate(productions)]))
Результат
В то время как в приведённом выше примере Анабены делаются только простые сравнения, в параметрических L-системах возможно любое выражение, использующее параметры в сочетании с арифметическими операторами +, −, ∗, /; оператором возведения в степень ∧, операторами отношения <, >, =; логическими операторами !, &, | (не, и, или) и т. д. Например, мы могли бы определить параметр time, значение которого увеличивается с каждой итерацией. Это можно использовать для увеличения ширины ветвей дерева.
Применение pyp5js для рисования на веб-страницах с помощью Python
Когда Линденмайер разработал L-систему, одним из немногих доступных языков программирования был Logo. Он включал в себя черепашью графику, и, таким образом, основа алгоритмического искусства был порождена переводом алфавита L-системы в команды для черепашки.В примерах из книги «The Nature of Code» фактически это было реализовано на языке Processing. Однако, вместо того чтобы ограничивать себя настольным компьютером, можно увеличить пробег, используя библиотеку p5js, которая представляет собой «...интерпретацию Processing для сегодняшнего Интернета». Чтобы продемонстрировать это, возьмём следующий пример:
AXIOM = 'G'
RULES = {
'F': 'FF',
'G': 'F-[[G]+G]+F[+FG]-G'
}
productions = [AXIOM]
generate_d0l(AXIOM, productions, iterations=5)
print(''.join([f"{i}: {p}\n" for i,p in enumerate(productions)]))
Результат
Саммер Ризо уже реализовал L-системы в библиотеке p5js, которая доступна на сайте thefractal.zone. Копирование аксиомы и правил, а также установка угла ветвления θ=30° сгенерируют приведённый ниже рисунок. Обратите внимание, что в коде показаны только 5 итераций, в то время как рисунок основан на большем количестве итераций (я поставил ползунок на максимум). При использовании рекурсии в Python можно быстро достигнуть предела: когда я пытался выполнить iterations=10 на стандартной виртуальной машине, блокнот Deepnote выдал ошибку IOPub data rate exceeded. Ещё одна проблема — чтение и применение оптимизации хвостовых вызовов в Python.
Рассмотрев алгоритмические и технические компоненты, мы можем собрать всё воедино. Есть ещё довольно много вещей, с которыми можно поэкспериментировать, чтобы создавать реалистичные эскизы, такие как добавление шума для рисования «схематичных» линий, увеличение толщины ветвей, добавление теней с засветкой и создание сцены с наложением деревьев.
Как питонист я был рад найти библиотеку pyp5js, которая позволяет повторно использовать идеи Processing в Python и перекодировать их в p5.js с помощью Transcrypt.
Такой подход охватывает все методы из p5.js и даёт нам достаточно инструментов для рисования, которыми можно поиграть. Но это предназначено для другой публикации. А пока я оставляю вас с генеративным творением Андерса Хоффа, которое может вдохновить вас на создание предметов генеративного искусства на Python.
Разработанный 27 лет назад Python 1.0 сегодня — не просто зрелый язык программирования, но язык в расцвете сил: у него есть не только развитая экосистема, но и философия, направленная на простоту и ясность программирования, а значит, расцвет языка будет продолжаться ещё долго.
Вместе с тем огромными темпами развивается веб: от веб-страниц мы пришли к веб-приложениям, иными словами браузер стал операционной системой в миниатюре. Если вам интересно поработать на стыке этих областей, вы можете присмотреться к курсу Fullstack-разработчик на Python, где студенты получают системную подготовку, приобретая все знания, необходимые для старта карьеры.
Источник статьи: https://habr.com/ru/company/skillfactory/blog/559390/