Primeira vez aqui? Seja bem vindo e cheque o FAQ!
x

Como replicar gráficos usando modelagem L-systems no Python?

0 votos
28 visitas
perguntada Mai 5 em Ciência da Computação por Carlos A T Haraguchi (1 ponto)  

É possível uma estratégia para implementação de modelos gráficos L-systems usando recursão e programação orientada a objeto?

Compartilhe

1 Resposta

0 votos
respondida Mai 5 por Carlos A T Haraguchi (1 ponto)  
editado Mai 14 por Carlos A T Haraguchi

Lindenmayer systems (L-systems) é uma ferramenta bastante versátil para modelagem de plantas. A partir de um objeto inicial (o axioma) e seguindo regras de rescrita (produção), objetos mais complexos são construídos. Exemplos e explicações sobre a ferramenta podem ser vistos nas respostas abaixo:

Como interpreto e gero representações tree OL- system?
Como desenhar árvores usando pilhas e recursões?
Como gerar imagens de árvores utilisando sistemas de Lindenmayer estocásticos?
Como replicar uma rose leaf usando os conhecidos L-Systems (Lindenmayer-Systems)?
Como replicar uma Sunflower core usando os conhecidos L-Systems (Lindenmayer-Systems)?
Como replicar estruturas de árvores com ramificações usando os conhecidos L-Systems (Lindenmayer-Systems)?
Como replicar Stochastic Plant usando os conhecidos L-Systems (Lindenmayer-Systems)?

Existem pelo menos dois aspectos que, ao meu ver, podem ser convenientemente explorados no momento da implementação de um L-system em um código Python. O primeiro, bem óbvio, é a recursão, já que o processo de rescrita segre uma regra predefinida. O segundo, talvez não tão óbvio, é o uso de classes e objetos (orientação a objetos), já que é possível agrupar as plantas, e suas partes, em famílias com características semelhantes.

Para exemplificar o uso da recursão e da orientação a objetos, usarei a replicação do padrão abaixo que foi extraído do livro The Algorithmic Beauty of Plants - Przemyslaw Prusinkiewicz and Aristid Lindenmayer (1991) [PL1991].

A imagem será apresentada aqui.

Perceba que o axioma (ômega) e a produção (p1, p2 e p3), além dos parâmetros que definem o atraso no desenvolvimento dos ramos laterais e a taxa de alongamento entre os nós da figura b, estão descritos abaixo das figuras. A tabela abaixo, também retirada do mesmo livro, apresenta os parâmetros utilizados para a construção de cada uma das figuras acima, incluindo o número de derivações.

image">

Iniciei o programa importando a classe Turtle e configurando a tela.

import turtle
# Configura a tela e a janela para encaixarem melhor os gráficos
screen = turtle.Screen()
screen.screensize(300,500)
turtle.setup(360,576)
turtle.delay(0) # para aumentar a velocidade da turtle

Então, utilizei a orientação a objetos, agrupando em classes os atributos (que descrevem) e os métodos (ações) que são característicos a determinados tipos de objetos. Além disso, para permitir herança, estruturei diversas classes.

A classe mais básica do exemplo é a L_system. A partir de qualquer L-system é possível extrair a "string" (cadeia de caracteres) que guiará a *turtle* no desenho do objeto. Além disso, para facilitar a implementação, a classe devia ter um método que transformasse a "string" em uma sequência de comandos. Note que o método *string* ainda não está realmente implementado nesta classe pois, embora seja comum a todos L-systems, somente será conhecido em grupos menores (nas classes que herdarão esta classe). Por outro lado, o método *commands* já podia ser implementado pois segue regras da sintaxe L-systems, comuns a todas as demais classes. Assim, mesmo que neste exemplo o "tradutor" não compreenda toda a sintaxe, ele pode ser atualizado a qualquer momento e afetar todas as demais classes herdeiras ao mesmo tempo!

# Classe abstrata L-system
class L_system:
    # Método: constrói a string L-system derivada de acordo com os parâmetros inicializados nesta classe
    def string(self):
        raise NotImplementedError("Please Implement this method")
    # Divide a string em comandos.
    # Obs.: este tradutor foi construído com base apenas nas regras deste 
    # exercício e, portanto, não compreende toda a sintaxe L-systems.
    def commands(self):
        commands = self.string()
        commands = commands.replace(')','|')
        commands = commands.replace('(',',')
        separators = ['[',']','+','-']
        for s in separators:
            commands = commands.replace(s, s+"|")
        return commands.split('|')

Em seguida, criei a classe de folhas, uma estrutura comum a diferentes tipos de plantas, que herda as características da classe L_systems. Esta classe é a Leaf, onde foi acrescentado outro método que calcula o comprimento da estrutura. Outras classes também poderiam ser criadas a partir da classe L_system por meio de herança. Por exemplo, seria possível criar uma classe Flower com um atributo *petals* que indicasse o número de pétalas.

# Classe abstrata Leaf
class Leaf(L_system):
    # Método: calcula o comprimento total do ramo principal (altura)
    def totallenght(self):
        raise NotImplementedError("Please Implement this method")

Mais específica, criei a classe das folhas características da família Umbelliferae que herda a classe Leaf. Esta classe, que se chama Umbelliferae, precisa, além dos atributos e métodos já implentados nas classes anteriores, também dos parâmetros de atraso no desenvolvimento dos ramos laterais, da taxa de alongamento dos ramos e do número de derivações. O axioma desta classe é fixo e, por isso, é criado no construtor. O grau de inclinação utilizado para produzir os ramos laterais também é fixo e criado no construtor, porém é um atributo que precisa ser acessado externamente pois será necessário para a turtle realizar seu trabalho.

# Classe abstrata Umbelliferae
class Umbelliferae(Leaf):
    # Construtor da classe
    def __init__(self, delay, rate, derivations):
        self.D = delay
        self.R = rate
        self.der = derivations
        self.deg = 45
        self.axiom = "A(0)"
    # Atributo: ângulo da rotação (em graus)
    def delta(self):
        return self.deg
    # Método: comprimento dos nós intermediários, dado o número de derivações
    # vividas pelo ramo.
    def F(self, a):
        return self.R**a

Para terminar esse conjunto de classes, foi criada a classe concreta Umbelliferae_symmetric, pois este exemplo trata de folhas simétricas. Esta classe herda a classe Umbelliferae. Basicamente, foram implementados métodos que já estavam definidos, mas ainda não implementados, nas classes anteriores. Um dos métodos implementados na classe Umbelliferae_symmetric é justamente a formação da "string", onde entra a recursão para cumprir as regras de produção. Note que o método __string foi encapsulado e é acessado externamente, de forma indireta, pelo método *string*.

# Classe concreta Umbelliferae
class Umbelliferae_symmetric(Umbelliferae): 
    # Método privado: constrói a string L-system
    def __string(self, _d, _der):
        # Caso exceção da recursão, quando não há derivação
        if self.der == 0:
            return self.axiom
        # Para sair quando terminar todas as derivações
        if _der == 0:
            return "A(" + str(_d) + ")"
        # Recorrência em cada derivação
        if _d == 0 or _der == self.der:
            # Aplica p2, substituindo A(0).
            # A aplicação de p3 é feita externamente usando o método F(), herdado
            # da classe Umbelliferae, para evitar o tratamento de arredondamento 
            # em casos de taxas de crescimento não inteiras.
            _der -= 1
            _d = self.D
            s = "F(" + str(_der) + ")[+" + self.__string(_d, _der) + \
                "][-" + self.__string(_d, _der) + \
                "]F(" + str(_der) + ")" + self.__string(0, _der)
        else:
            # Aplica p1, substituindo A(D>0)
            _der -= 1
            s = self.__string(_d-1, _der)
        return s
    # Método: constrói a string L-system derivada de acordo com os parâmetros inicializados nesta classe
    def string(self):
        return self.__string(self.D, self.der)
    # Método: calcula o comprimento total do ramo principal (altura)
    def totallenght(self):
        return 2*(self.R**self.der-1)/(self.R-1)

A recursão foi empregada no método __string da classe **Umbelliferae_symmetric**. O processo se encerra se não houver derivações ou se todas as derivações forem realizadas. A recursividade está na aplicação das produções p1 e p2. Para evitar o tratamento de arredondamento (e/ou um número com muitas casas decimais) quando o fator de crescimento não for inteiro, a produção p3, embora também pudesse ser recursiva, é aplicada externamente por um método da própria classe.

Outra classe foi utilizada na solução, Stack, mas confesso que copiei da solução em Como gerar imagens de árvores utilisando sistemas de Lindenmayer estocásticos? para servir de pilha na função que desenha o gráfico. Também copiei as funções que recuperam e que modificam o sentido e a posição da turtle.

# Classe pilha (somente com os métodos utilizados neste exemplo)
class Stack:
    # Construtor da classe
    def __init__(self):
        self.items = []
    # Método: acrescenta o item à pilha
    def push(self, item):
        self.items.append(item)
    # Método: retira item da pilha
    def pop(self):
        return self.items.pop()

# Função que recupera a tupla (direção/sentido, posição)
def get_turtle_state(t_obj):
    return t_obj.heading(), t_obj.position()

# Função que modifica a direção/sentido e a posição da turtle
def set_turtle_state(t_obj, state):
    t_obj.setheading(state[0])
    t_obj.setposition(state[1][0], state[1][11])

Criei a função Draw que desenha o gráfico usando um objeto Turtle e um objeto Umbelliferae_symmetric, podendo configurar a posição inicial e a escala do gráfico. O fundamental desta parte é a interpretação da string L-system para a classe Turtle, lembrando que implementei apenas os comandos que necessitava para esse exemplo.

# Função que desenha o gráfico usando a classe Turtle e seguindo a string.
# Obs.: esta função foi construída com base apenas nas regras deste exercício e,
# portanto, não compreende toda a sintaxe L-systems.
def Draw(t, L_object, pos=0, scale=1):
    if pos == 0:
        # Posiciona o início na parte inferior da tela
        pos = (0,-screen.screensize()[1]*0.9/2)
    adjust = screen.screensize()[1]*0.9/L_object.totallenght() # Fator para ajustar o tamanho à janela
    t.speed(0)     # Para aumentar a velocidade
    t.hideturtle() # Para aumentar a velocidade
    # Direciona a tartaruga para cima e a posiciona na parte inferior, sem traçar
    t.penup()
    set_turtle_state(t,(90, pos))
    t.pendown()
    t_state = Stack() # Objeto pilha que armazena as últimas posições da tartaruga que têm de ser retomadas
    commands = L_object.commands() # Busca os comandos
    # Iteração com os comandos para o turtle
    for command in commands:
        c = command[:1]
        if c == 'F':  # desenha uma linha no comprimento do segmento
            length = L_object.F(int(command.split(',')[1]))
            t.forward(scale*adjust*length)
        elif c == '[': # guarda posição na pilha para retormar depois
            t_state.push(get_turtle_state(t))
        elif c == '+': # gira a direção da tartaruga em 45 graus à esquerda
            t.left(L_object.delta())
        elif c == '-': # gira a direção da tartaruga em 45 graus à direita
            t.right(L_object.delta())
        elif c == ']':  # busca a última posição guardada na pilha para retomar o ramo pendente
            set_turtle_state(t, t_state.pop())

Por fim, para gerar a replicação, rodei as linhas abaixo.

# Desenha a Umbelliferae
t = turtle.Turtle()
A = Umbelliferae_symmetric(0, 2, 3)  # estrutura menor
Draw(t, A, (-120,80), 0.3)           # desenha a estrutura menor no canto superior esquerdo, 30% do tamanho original
A = Umbelliferae_symmetric(0, 2, 10) # estrutura maior
Draw(t, A)                           # desenha a estrutura maior na posição default

# Permite fechar a janela e rodar novamente o programa
screen.exitonclick()
turtle.done()

O resultado final foi este:

A imagem será apresentada aqui.

Algumas observações.

  1. Utilizei, na última parte do código os parâmetros D, R e
    derivation length do modelo a. Você pode testar os outros
    modelos de folha usando os parâmetros apresentados no início, ou
    mesmo outros parâmetros à sua escolha. Lembre-se apenas que quanto
    maior o número de derivações (derivation length) e menor o
    atraso no crescimento (D), maior será o número de comandos que a
    classe Turtle terá de executar e esta é a parte mais lenta
    do programa.
  2. No livro, há também um modelo assimétrico (figura 5.12,
    com ramos alternados). Se as classes e funções acima foram
    corretamente construídas, bastaria criar uma nova classe
    (Umbelliferae_alternating, por exemplo) com os mesmos métodos da
    classe Umbelliferae_symmetric, implementando porém a
    recursão para atender o novo modelo e passando uma instância dessa
    nova classe à já implementada função Draw.
comentou Mai 9 por danielcajueiro (5,501 pontos)  
Muito interessante o problema. Use o botão de hiperlink para incluir links. Ele é o terceiro botão da esquerda para a direita que aparece quando vc vai colocar uma resposta ou uma pergunta.
comentou Mai 14 por Carlos A T Haraguchi (1 ponto)  
Obrigado pelo aviso. Os hiperlinks foram corrigidos.
...