Python自学系列--使用LS文法绘制分形图形

一、前言

       作为一名python语言的初学者,在看其官方文档时,看到标准库里有一个海龟画图,感觉挺好玩的。于是研究了一下,便依葫芦画瓢,自己画一个简单的棒棒糖 🍭(画了哄小孩子的)。只是这个海龟画图好象有些眼熟,努力回忆,想起自己好几年前看过一本《分形算法与程序设计–Java实现》(2003年出版的,孙博文编著),里面介绍了一种LS文法,和这很相似。那本书比较老,因此里面的代码也是比较古老的,还是使用的applet。那本书里介绍了几种分形算法,其中LS文法与海龟绘图原理一样。既然要自学python,那就要编码实践,所以打算使用python的海龟绘图来实现这个LS文法绘制。

二、LS文法

       文法构图算法是仿照语言学中的语法生成方法来构造图形的一种算法。

       美国著名语言学家乔姆斯基(N.Chomsky)在20世纪50年代给出了递归生成语法的方法:指定一个或者几个初始字母和一组“生成规则”,将生成规则反复作用到初始字母和新生成的字母上,产生出整个语言。这就是由“生成语法”定义的形式语言,例如:

       字母表: L,R
       生成规则: L -> R,R -> LR
       初始字母: R

       则有 R -> LR -> RLR -> LRRLR -> RLRLRRLR -> LRRLRRLRLRRLR ->…

       LS文法于1984年由A.R.Smith首次引入到计算机图形学领域。

       在二维平面上,LS文法的图形生成过程,类似于海龟在沙滩上行走。海龟行走的每一时刻的状态定义为当前位置矢量T与前进方向角δ的集合(T,δ),则二维LS文法字母表的绘图规则如下:

  • F:在当前方向前进一步,并画线
  • f :在当前方向前进一步,不画线
  • +:逆时针旋转一个角度δ
  • -:顺时针旋转一个角度δ
  • [ :将当前信息压栈
  • ] :将[时刻的信息出栈

       举一个拟在后面实现的例子:Koch曲线。它的LS文法如下:

  • ω:F
  • δ:60°
  • P:F -> F+F–F+F

则有:

  • 步骤0:F
  • 步骤1:F+F–F+F
  • 步骤2:F+F–F+F+F+F–F+F–F+F–F+F+F+F–F+F
  • 步骤3:F+F–F+F+F+F–F+F–F+F–F+F+F+F–F+F+F+F–F+F+F+F–F+F–F+F–F+F+F+F–F+F–F+F–F+F+F+F–F+F–F+F–F+F+F+F–F+F+F+F–F+F+F+F–F+F–F+F–F+F+F+F–F+F

三、python下简单实现

       python实现需要利用海龟绘图这个模块。因为LS文法可以画很多图形,所以为了复用,先写LS方法模块。

       打开IDLE,新建一个文件,输入如下代码:

# LS文法生成图形的库(公用)
from turtle import *


# 初始化原点及画笔
def init(x,y,_speed):
    ht()
    up()
    speed(_speed)
    setx(x)
    sety(y)
    color('red')
    down()


# 在当前方向向前走一步,并画线
def drawF(step):
    forward(step)


# 在当前方向向前走一步,不画线
def drawf(step):
    up()
    forward(step)
    down()


# 反时针旋转a度
def drawPlus(a):
    left(a)


# 顺时针旋转a度
def drawMinus(a):
    right(a)


# 压栈
def push():
    pass


# 出栈
def pop():
    pass


def draw(ls,step,angel,x,y,_speed):
    init(x,y,_speed)
    for c in ls:
        if c == 'F':
            drawF(step)
        elif c == 'f':
            drawf(step)
        elif c == '+':
            drawPlus(angel)
        elif c == '-':
            drawMinus(angel)
        elif c == '[':
            push()
        elif c == ']':
            pop()
        else:
            print("invalid char:",c)
    done()


# LS文案生成,返回一个list
# 参数分别为初始条件(list),规则(字典)和迭代次数
def getLs(origin,rule,n):
    L = []
    for key in origin:
        if key in rule:
            des = rule[key]
            L += list(des)
        else:
            L.append(key)
    if n == 1:
        return L
    else:
        return getLs(L,rule,n-1)


Draw,GetLs = (draw,getLs)

       这其中getLs是递归调用来生成LS文法字符串,压栈和出栈操作现在还暂缺。第一现阶段没有用到[],第二作为一个初学者,没有反复尝试、实际运行和来回修改我也没办法凭空写出相应的代码。

       将文件保存为drawLs.py,然后再新建一个文件,用来定义Koch曲线的初始规则和条件。

# Koch 曲线LS方法生成
from drawLs import Draw,GetLs


# 定义初始变量
step = 20   # 步长
n = 3        #迭代次数
# 初始条件
origin = 'F'
angel = 60
# 迭代规则
rule = { 'F' : "F+F--F+F" }
# 初始坐标
x = -300
y = 0
# 动画速度
speed = 'normal'


if __name__ == "__main__":
    ls = GetLs(list(origin),rule,n)
    Draw(ls,step,angel,x,y,speed)

       保存为同一目录下的Koch_ls.py。然后点击IDLE菜单上的runRun Module,一条红色的Koch曲线就慢慢的画出来了。
在这里插入图片描述
       改变初始条件或者初始规则就可以得到不同的图形,有兴趣的读者可以多做尝试。
比如下例条件:

# 初始条件
origin = 'F++F++F'
angel = 60
# 迭代规则
rule = { 'F' : "F+F--F+F" }

画出的图形如下:
在这里插入图片描述

四、增加压栈和出栈操作

       上面的Koch曲线是一条连续的曲线,可以利用一只画笔一气呵成不间断画完。但是如果我们想画一个如下的树,有主干有分叉,一只画笔不间断绘画就无法完成了。
在这里插入图片描述
       这里我们需要在分支的地方保存当前的画笔状态,也就是将画笔入栈。在分支画完以后取出栈中的画笔作为主干画笔再接着画。

       一起来看这个分形树的初始条件和规则:

  • ω:F
  • δ:25°
  • P:F -> F[-F]F[+F]F

       首先需要改写drawLs.py,加入当前画笔和堆栈相关的操作,完成后的代码如下:

# LS文法生成图形的库(公用)
from turtle import *

# 全局变量
stack = []
class Pen:
    current:None


# 初始化原点及画笔
def init(x,y,_speed):
    ht()
    up()
    Pen.current = getpen()
    speed(_speed)
    setx(x)
    sety(y)
    color('red')
    down()


# 在当前方向向前走一步,并画线
def drawF(step):
    Pen.current.forward(step)


# 在当前方向向前走一步,不画线
def drawf(step):
    Pen.current.up()
    Pen.current.forward(step)
    Pen.current.down()


# 反时针旋转a度
def drawPlus(a):
    Pen.current.left(a)


# 顺时针旋转a度
def drawMinus(a):
    Pen.current.right(a)


# 压栈
def push():
    pen = Pen.current.clone()
    stack.append(pen)


# 出栈
def pop():
    Pen.current = stack.pop()


def draw(ls,step,angel,x,y,_speed):
    init(x,y,_speed)
    for c in ls:
        if c == 'F':
            drawF(step)
        elif c == 'f':
            drawf(step)
        elif c == '+':
            drawPlus(angel)
        elif c == '-':
            drawMinus(angel)
        elif c == '[':
            push()
        elif c == ']':
            pop()
        else:
            print("invalid char:",c)
    done()


# LS文案生成,返回一个list
# 参数分别为初始条件(list),规则(字典)和迭代次数
def getLs(origin,rule,n):
    L = []
    for key in origin:
        if key in rule:
            des = rule[key]
            L += list(des)
        else:
            L.append(key)
    if n == 1:
        return L
    else:
        return getLs(L,rule,n-1)


Draw,GetLs = (draw,getLs)

       可以看到,改写后对外的接口没有变化,运行Koch_ls.py,你仍然会得到相同的结果。代码中使用了一个全局变量来记录当前画笔,压栈时就克隆当前画笔然后保存,出栈时就将弹出的画笔作为当前画笔。作为一个python初学者,我在全局变量引用这里也耽误了一点时间(因为我也没有仔细看过文档)。全局变量在函数中除非声明为global,否则只能读取而不能直接赋值改变(可以间接改变)。直接给全局变量赋值相当于创建了一个同名的局部变量,这一点和其它语言不相同,还是有些不习惯。

       接着实现上面的分形树,再新建一个Tree_ls.py,代码如下:

# 分形树实现
from drawLs import Draw,GetLs

# 定义初始变量
step = 10  # 步长
n = 3       #迭代次数
# 初始条件
origin = 'F'
angel = 25
# 迭代规则
rule = { 'F' : "F[-F]F[+F]F" }
# 初始坐标
x = 0
y = 0
# 动画速度
speed = 'fast'


if __name__ == "__main__":
    ls = GetLs(list(origin),rule,n)
    Draw(ls,step,angel,x,y,speed)

画出的树如下图:
在这里插入图片描述
       可以看到,我们的树变成横向生长了,这是因为我们的画笔(海龟)的初始方向是水平的,接下来我们对它进行完善。

五、进一步完善

计划完善的地方有:

  • 可以提供初始方向(角度)
  • 可以增加渲染速度

我们给drawLs.py中的draw()方法增加两个参数,head_tracer,分别代表初始方向和渲染的条件。相应的也要修改该文件的其它部分代码,修改完成后代码如下:

# LS文法生成图形的库(公用)
from turtle import *

# 全局变量
stack = []
class Pen:
    current:None


# 初始化原点及画笔
def init(x,y,_speed,head,_tracer=None):
    ht()
    up()
    Pen.current = getpen()
    speed(_speed)
    if _tracer:
        tracer(_tracer,0)
    setx(x)
    sety(y)
    color('red')
    seth(head)
    down()


# 在当前方向向前走一步,并画线
def drawF(step):
    Pen.current.forward(step)


# 在当前方向向前走一步,不画线
def drawf(step):
    Pen.current.up()
    Pen.current.forward(step)
    Pen.current.down()


# 反时针旋转a度
def drawPlus(a):
    Pen.current.left(a)


# 顺时针旋转a度
def drawMinus(a):
    Pen.current.right(a)


# 压栈
def push():
    pen = Pen.current.clone()
    stack.append(pen)


# 出栈
def pop():
    Pen.current = stack.pop()


def draw(ls,step,angel,x,y,_speed,head,_tracer):
    init(x,y,_speed,head,_tracer)
    for c in ls:
        if c == 'F':
            drawF(step)
        elif c == 'f':
            drawf(step)
        elif c == '+':
            drawPlus(angel)
        elif c == '-':
            drawMinus(angel)
        elif c == '[':
            push()
        elif c == ']':
            pop()
        else:
            print("invalid char:",c)
    if _tracer:
        update()
    done()


# LS文案生成,返回一个list
# 参数分别为初始条件(list),规则(字典)和迭代次数
def getLs(origin,rule,n):
    L = []
    for key in origin:
        if key in rule:
            des = rule[key]
            L += list(des)
        else:
            L.append(key)
    if n == 1:
        return L
    else:
        return getLs(L,rule,n-1)


Draw,GetLs = (draw,getLs)

我们在Tree_ls.py中也增加这两个初始条件:

# 分形树实现
from drawLs import Draw,GetLs

# 定义初始变量
step = 6  # 步长
n = 4       #迭代次数
# 初始条件
origin = 'F'
angel = 25
# 迭代规则
rule = { 'F' : "F[-F]F[+F]F" }
# 初始坐标
x = 0
y = -200
# 动画速度
speed = 'fast'
# 初始角度
head = 90
# 渲染速度
tracer = 5


if __name__ == "__main__":
    ls = GetLs(list(origin),rule,n)
    Draw(ls,step,angel,x,y,speed,head,tracer)

我们将初始角度设定为正北方,并且加快了渲染速度,生成图形如下:
在这里插入图片描述
       然而这里面也遇到了一个坑,如果你使用了trace()方法来设定渲染条件,那么只有符合条件的绘制渲染出来了,未符合条件的绘制并没有显示。作为一个初学者,我也是有些蒙圈😂😂😂。后来仔细看了一下文档,看到介绍有update()方法,试了下,果然如此🤝🤝🤝。它的作用是在你设置渲染条件的情况下强制刷新一下,这样所有的部分都会显示出来。

       同样,我们修改Koch_ls.py的代码来增加这两个初始条件:

# Koch 曲线LS方法生成
from drawLs import Draw,GetLs


# 定义初始变量
step = 10   # 步长
n = 4        #迭代次数
# 初始条件
origin = 'F++F++F'
angel = 60
# 迭代规则
rule = { 'F' : "F+F--F+F" }
# 初始坐标
x = -300
y = -350
# 动画速度
speed = 'fast'
# 初始角度
head = 0
# 渲染速度
tracer = 5

if __name__ == "__main__":
    ls = GetLs(list(origin),rule,n)
    Draw(ls,step,angel,x,y,speed,head,tracer)

六、绘制其它图形

       让我们任意改变一下分形树的规则,将 F -> F[-F]F[+F]F 后面加一个F改成 F -> F[-F]F[+F]FF。生成的图形如下:
在这里插入图片描述
       接着上面的修改,将初始条件变成origin = 'F[+F][-F]F',生成的图形如下图:
在这里插入图片描述
       柳枝的绘制:

  • ω:F
  • δ:20°
  • P:F -> F[-F]F[+F]-F
    在这里插入图片描述
           手帕的绘制:
# 定义初始变量
step = 20 # 步长
n = 3       #迭代次数
# 初始条件
origin = 'F+F+F+F'
angel = 90
# 迭代规则
rule = { 'F' : "F[F]+F-F[++F]-F+F" }
# 初始坐标
x = 200
y = -200
# 动画速度
speed = 'fast'
# 初始角度
head = 90
# 渲染速度
tracer = 250

在这里插入图片描述

七、总结

       LS文法绘制分形图形是先多次迭代后生成图形的路径,然后用画笔分别描绘出来。这里只学习了LS单规则文法,还有多规则文法。多规则文法简单的讲就是增加规则表中的字母种类,有的只作替换(比如X),有的既作替换,也做绘制(比如F)。有兴趣的读者可以自己阅读一下相关书籍。

       这里的LS文法路径是预先生成好并保存在list中的长串字符,当规则复杂,迭代次数很多时并不是很高效。下一步看能否用一个生成器代替。

       欢迎大家指出错误或者提出改进意见。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AiMateZero

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值