形式文法とアルゴリズム


以前の投稿に続き,今回はアリステッド・リンデンマイヤーという人が提唱したL-systemというもののシミュレーションを行ってみたい.これは再帰的に文字列を書き換えることで、自己相似性やフラクタル的な複雑なパターンを簡潔に表現できる.

import math
import matplotlib.pyplot as plt
import imageio
import io
import numpy as np

# 文字列の次世代を生成する関数
def generate_next_string(current, rules):
    next_string = ""
    for ch in current:
        if ch in rules:
            next_string += rules[ch]
        else:
            next_string += ch
    return next_string

# タートル解釈を行い、線分のリストを返す関数
def interpret_lsystem(instructions, angle_deg, line_length):
    x, y = 0, 0
    angle = 90  # 初期角度:上向き
    segments = []
    stack = []
    for cmd in instructions:
        if cmd == 'F':
            new_x = x + line_length * math.cos(math.radians(angle))
            new_y = y + line_length * math.sin(math.radians(angle))
            segments.append(((x, y), (new_x, new_y)))
            x, y = new_x, new_y
        elif cmd == 'G':
            x += line_length * math.cos(math.radians(angle))
            y += line_length * math.sin(math.radians(angle))
        elif cmd == '+':
            angle += angle_deg
        elif cmd == '-':
            angle -= angle_deg
        elif cmd == '[':
            stack.append((x, y, angle))
        elif cmd == ']':
            x, y, angle = stack.pop()
        # 他の文字は無視
    return segments

# 得られた線分を描画し、画像(numpy配列)として返す関数
def plot_segments(segments, margin=10):
    # segmentsが空の場合は背景だけの画像を作成
    if not segments:
        fig, ax = plt.subplots(figsize=(6,6))
        ax.set_aspect('equal')
        ax.axis('off')
        ax.set_xlim(-margin, margin)
        ax.set_ylim(-margin, margin)
        buf = io.BytesIO()
        plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0)
        buf.seek(0)
        img = imageio.v2.imread(buf)
        plt.close(fig)
        return img
    
    xs, ys = [], []
    for seg in segments:
        (x1, y1), (x2, y2) = seg
        xs.extend([x1, x2])
        ys.extend([y1, y2])
    xmin, xmax = min(xs), max(xs)
    ymin, ymax = min(ys), max(ys)
    
    fig, ax = plt.subplots(figsize=(6,6))
    for seg in segments:
        (x1, y1), (x2, y2) = seg
        ax.plot([x1, x2], [y1, y2], color='black', linewidth=1)
    ax.set_aspect('equal')
    ax.axis('off')
    ax.set_xlim(xmin - margin, xmax + margin)
    ax.set_ylim(ymin - margin, ymax + margin)
    
    buf = io.BytesIO()
    plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0)
    buf.seek(0)
    img = imageio.v2.imread(buf)
    plt.close(fig)
    return img

# 各フレームのサイズを統一するためにパディングする関数
def pad_frames(frames):
    max_h = max(frame.shape[0] for frame in frames)
    max_w = max(frame.shape[1] for frame in frames)
    padded_frames = []
    for frame in frames:
        h, w = frame.shape[:2]
        pad_bottom = max_h - h
        pad_right = max_w - w
        # 画像の下部と右側に余白(白背景)を追加
        padded = np.pad(frame, ((0, pad_bottom), (0, pad_right), (0, 0)),
                        mode='constant', constant_values=255)
        padded_frames.append(padded)
    return padded_frames

# L-systemの各世代の描画を行い、GIFを出力する関数
def lsystem_to_gif(axiom, rules, angle_deg, initial_length, iterations, scale_factor, gif_filename):
    frames = []
    current_string = axiom
    line_length = initial_length
    for i in range(iterations + 1):
        print(f"Iteration {i}, string length: {len(current_string)}")
        segments = interpret_lsystem(current_string, angle_deg, line_length)
        img = plot_segments(segments)
        frames.append(img)
        current_string = generate_next_string(current_string, rules)
        line_length *= scale_factor  # 線分長さの縮小
    # すべてのフレームのサイズを統一するためにパディング
    frames = pad_frames(frames)
    imageio.mimsave(gif_filename, frames, duration=0.5)
    print("GIF saved as", gif_filename)

# メイン処理
if __name__ == "__main__":
    # Fractal Plant の例
    # 公理:X
    # 規則:
    #   X -> F-[[X]+X]+F[+FX]-X
    #   F -> FF
    # 回転角:25°
    axiom = "X"
    rules = {
        "X": "F-[[X]+X]+F[+FX]-X",
        "F": "FF"
    }
    angle_deg = 25
    initial_length = 100  # 初期線分長さ
    iterations = 5        # 世代数(0世代~5世代)
    scale_factor = 0.5    # 各世代ごとの線分長さの縮小率
    gif_filename = "lsystem_simulation.gif"
    
    lsystem_to_gif(axiom, rules, angle_deg, initial_length, iterations, scale_factor, gif_filename)

参考文献

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です