使用python生成模拟花朵生长的gif动图(Simulate flower grow process with python)
在网上读到刘大可的文章《关于“植物身上的黄金分割”的叫你恍然大悟的文章》,里面的动图非常有趣,本文是自己尝试用python语言生成这些动图的过程。
原理
首先要理解花瓣的生长过程,上面的文章中有一段话:
图1: 图片来源:https://card.weibo.com/article/m/show/id/2309404407466321379574
那么如果以坐标原点为”顶端“,(x0,y0), (x1,y1)分别为先后分生出的新牙,那么如果夹角值固定,花瓣每次向外展开的长度是固定的,我们就可以简化为下面的高中数学问题:
其中a, b表示长度,是已知值,并且根据上面的假设,a=2b,python有专门求解(x1,y1)的库,我们要做的只是列出方程,在这里略去推导过程,求解x1,y1的python代码如下:
其中ratio是图1中两个角度的比值。
实现
问题简化为给定两个点以及它们的夹角,求出第三个点,然后讲这第三个点加入到列表,当作新的x0,y0,和原点联合起来,再求出新的x1,y1。 在这个过程中,每个点还以相等的速度远离远点。这样反复迭代,就得到了花朵在每个时间点的所有点的坐标,每个时间点生成一张图片,把所有的图片制作成gif,就得到了上面的过程,主要代码如下:
from __future__ import division
from PIL import Image, ImageDraw
from images2gif import writeGif
import math
import numpy
import sympy as sym
pi = 3.1415926
def solve(a, b, x0, y0, ratio):
theta = (360.0 * ratio) / (1.0 + ratio)
if theta > 180.0:
theta = 360 - theta
angle = theta / 360.0 * 2 * pi
x1, y1 = sym.symbols('x1,y1')
f = sym.Eq(x1**2 + y1**2, b**2)
g = sym.Eq(x0 * x1 + y0 * y1, a * b * math.cos(angle))
return sym.solve([f, g], (x1, y1))
def extend(baseX, baseY, x, y, extend):
signX_head = 1
signY_head = 1
if baseX > x:
signX_head = -1
else:
signX_head = 1
if baseY > y:
signY_head = -1
else:
signY_head = 1
length = math.sqrt((x-baseX)**2 + (y-baseY)**2)
if length == 0:
return (baseX, baseY)
lfCos = math.fabs(math.fabs(x - baseX) / length)
lfSin = math.fabs(math.fabs(y - baseY) / length)
dx = extend * lfCos
dy = extend * lfSin
x = x + dx * signX_head
y = y + dy * signY_head
return (x, y)
def create_image_with_ball(width, height, points, ball_size):
img = Image.new('RGB', (width, height), (255, 255, 255))
draw = ImageDraw.Draw(img)
# draw.ellipse takes a 4-tuple (x0, y0, x1, y1) where (x0, y0) is
# the top-left bound of the box and (x1, y1) is the lower-right
# bound of the box.
for point in points:
topLeftX = point[0] + width / 2.0
topLeftY = -point[1] + height / 2.0
lowRightX = point[0] + ball_size + width / 2.0
lowRightY = -point[1] + ball_size + height / 2.0
draw.ellipse((topLeftX, topLeftY, lowRightX, lowRightY), fill='black')
return img
def isSameLine(x1, y1, x2, y2, x3, y3):
return math.fabs((y1 - y2) * (x1 - x3) - (y1 - y3) * (x1 - x2)) <= 1e-3
if __name__ == '__main__':
x0, y0 = 0, 0
deltaLen = 5
format = "flowerGrow_ratio_random__%s.gif"
# ratios = [1.618, 0.36, 0.25316, 1.41421, 0.5, 0.2, 0.16666]
# for ratio in ratios:
for ratio in numpy.arange(0.1525 , 2 , 0.1):
points = [(x0, y0)]
frames = []
for index in range(100):
if len(points) == 1:
points.append((0, 0 + deltaLen))
elif len(points) == 2:
points[1] = extend(x0, y0, points[1][0], points[1][1], deltaLen)
a = deltaLen * 2
b = deltaLen
towPoints = solve(a, b, points[1][0], points[1][1], ratio)
points.append(towPoints[0])
else:
for i in range(len(points)):
if i == 0:
continue
points[i] = extend(x0, y0, points[i][0], points[i][1], deltaLen)
lastPoint = points[-1]
x1 = lastPoint[0]
y1 = lastPoint[1]
lastSecond = points[-2]
lenA = math.sqrt((x1-x0)**2 + (y1-y0)**2)
lenB = deltaLen
twoPoints = solve(lenA, lenB, x1, y1, ratio)
if isSameLine(x0, y0, lastSecond[0], lastSecond[1], twoPoints[0][0], twoPoints[0][1]):
points.append(twoPoints[1])
else:
points.append(twoPoints[0])
new_frame = create_image_with_ball(800, 800, points, 20)
frames.append(new_frame)
# Save into a GIF file that loops forever
filename = format % ratio
print format % ratio
writeGif(filename, frames, duration=0.2)
几点说明:
- images2gif来自https://github.com/lucyking/images2gif-Pillow,用来生成gif
- extend函数用来求解每个点远离的过程,其中(baseX, baseY)是基准点,在这里是原点
- 由于solve函数求解出来有两组解,花朵的生长不可能是堆叠式生长,新长出的花瓣方向和原点,x0,y0的方向必须不在一条直线上。
效果图
Ratio=1.618,黄金分割率的效果,可以看到花瓣分布均匀,空间利用率高:
Ratio=1.7525
Ratio=0.8525
Ratio=1.25
Ratio=1.0525