7

用Python动画来展示二阶贝赛尔曲线

 3 years ago
source link: https://zhuanlan.zhihu.com/p/130275591
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

用Python动画来展示二阶贝赛尔曲线

上海交通大学 计算机应用技术硕士

说起来贝氏曲线,我们可能首先会想到下面这个男人:

v2-bbc277403b88236c9cde5c2bd9b91004_720w.jpg
图1. 一位不愿透露姓名的英国球员

但实际上我们说的不是这个叫贝克汉姆的英国男人,而是另外一个人,就是下面这个叫“皮埃尔·贝塞尔”(Pierre Bézier)的法国男人:

图2. 皮埃尔·贝塞尔

贝塞尔论起知名度,也许不如小贝,但说起对人类的贡献,那可是杠杠的,著名的“贝塞尔曲线”就出自他之手。1962年,贝塞尔发表了贝塞尔曲线的相关理论研究,当时在雷诺公司工作的他,主要运用贝塞尔曲线进行汽车设计。说到这里可能还是有很多人没明白贝塞尔曲线到底是什么,看一下下面这个图,大家就明白了。

图3. 设计中用到的贝赛尔曲线

在Photoshop等多种设计软件中,画曲线时主要用到的是就是贝赛尔曲线,就是类似于上图中的这个曲线,设计师们可以通过控制中间的控制点来画出自己需要的曲线。早先设计师们想要用电脑画出一条直线灰常简单,但要画出一条平滑的曲线却非常难,而贝塞尔曲线的诞生,让大家用电脑绘制出一条平滑曲线成为了现实,这也就是贝塞尔曲线的最大用途。

而今天我们就来说一下最简单的二阶贝塞尔曲线的推导,并用matplotlib进行展示。(实际上最简单的是一阶,但因为其只有一条直线,所以没有什么实际用途,就忽略了)

我们先来了解一下二阶贝赛尔曲线的原理。假如连在一起的两条线段AB和BC,如下图:

图4. 二阶贝赛尔曲线原理图 1

现在AB上取一点D,BC上取一点E,使得AD/AB=BE/BC,如下图:

图5. 二阶贝赛尔曲线原理图 2

而在线段DE上还要求一点F,使得DF/DE=AD/AB=BE/BC,如下图:

图6. 二阶贝赛尔曲线原理图 3

而当D在AB上不断移动,E在BC上不断移动,形成的F点的轨迹便是一条曲线,这条曲线就是二阶贝塞尔曲线。这就是今天我们要推导并演示的曲线。

下面直接用Python代码来展示一下。首先还是导入各种包:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation

因为我们用的是matplotlib来做的演示,所以要设置一下matplotlib的后端,也就是显示方式。这一行代码最好单独使用,否则容易失效,所以把它单独列出来:

%matplotlib

这是非常重要的一步,这一步能让matplotlib在绘图时弹出一个新窗口,而不是在原窗口直接绘制,因为原窗口无法显示动画,这里用了ipython的magic 命令,也就是在一个命令前面加上“%”。在这里说明一下,笔者用的是win7系统,开发工具为Anaconda最新版,可以直接去Anaconda官网下载。

因为我们要用到A、B、C三个点,所以要设置一下三个点坐标。A点坐标为x1和y1,B点为x2和y2,C点为x3和y3,再设置一个区间内点的个数dots_num,这个dots_num后面会有解释。所有这些变量的值都可以随意设定,但有一些要求,因为笔者把绘图的坐标系设定在100,也就是x轴和y轴的范围都是0—100,所以上面三个点的坐标都不要超过这个范围,而dots_num的数量尽可能大一些,做动画时更连贯一些,所以上面这些参数的设置如下:

x1=10
y1=80
x2=50
y2=10
x3=90
y3=80
dots_num=100

接下来是获得贝塞尔曲线的轨迹的函数:

def two_degree_bc(x1=10, y1=80, x2=50, y2=10, x3=90, y3=80, dots_num=100): #bezier curve
    global xt, yt, x_dots12, x_dots23, y_dots12, y_dots23
    xt = [] #目标点的x坐标
    yt = [] #目标点的y坐标
    x_dots12 = np.linspace(x1, x2, dots_num) #线段AB的x坐标
    y_dots12 = np.linspace(y1, y2, dots_num) #线段AB的y坐标
    x_dots23 = np.linspace(x2, x3, dots_num) #线段BC的x坐标
    y_dots23 = np.linspace(y2, y3, dots_num) #线段BC的y坐标
    for i in range(dots_num): #获得目标点的轨迹
        x = x_dots12[i] + (x_dots23[i]-x_dots12[i])*i / (dots_num-1)
        y = y_dots12[i] + (y_dots23[i]-y_dots12[i])*i / (dots_num-1)
        xt.append(x)
        yt.append(y)

这里的xt和yt是两个list,就是用来存放目标点的x坐标和y坐标,而x_dots12和y_dots12分别是线段AB的x和y坐标,x_dots23和y_dots23分别是线段BC的x和y坐标,这四个是numpy的array格式,所有这些数据都设置成全局变量。而从上面的代码中我们可以看到变量dots_num的作用是在线段AB和BC上取这么多的点,然后用这些点推导目标点的坐标,点的数量越多,目标点的坐标也就越多,绘制出来的曲线也就更平滑。

接下来是动画函数,这个函数后面再解释:

def run(i):
    art1.set_data(x_dots12[i], y_dots12[i])
    art2.set_data(x_dots23[i], y_dots23[i])
    art3.set_data([x_dots12[i], x_dots23[i]], [y_dots12[i], y_dots23[i]])
    art4.set_data(xt[i], yt[i])
return art1,art2,art3,art4

最后就是绘制动画了,代码如下:

two_degree_bc() #先生成目标点的轨迹
fig, ax = plt.subplots(figsize=(8,8))
ax.set_aspect(1) #让两个坐标轴等比例
plt.xlim([0,100]) #设置坐标轴范围
plt.ylim([0,100])
ax.plot([x1, x2], [y1, y2], color='#3e82fc') #绘制AB线段
ax.plot([x2, x3], [y2, y3], color='#3e82fc') #绘制BC线段
ax.plot(xt,yt,color='orange') #绘制目标曲线
art1, = ax.plot(x_dots12[0], y_dots12[0], color='green', marker='o') #不能用scatter,因为得到的对象不是一个list,是一个object
art2, = ax.plot(x_dots23[0], y_dots23[0], color='green', marker='o')
art3, = ax.plot([x_dots12[0], x_dots23[0]], [y_dots12[0], y_dots23[0]], color = 'purple') #plot得到的结果是一个list,只包含一个元素,即一个形状object
art4, = ax.plot(xt[0], yt[0], color='red', marker='o')
 
ani = animation.FuncAnimation(
    fig, run, frames=range(100), interval=2, save_count=50)
plt.show()

这里首先运行two_degree_bc()函数得到目标点的轨迹,然后绘制AB、BC线段以及目标曲线,这些都是静态图。接着从变量art1开始就是绘制动画的部分了。这部分比较复杂,一共有art1、art2、art3和art4这四个变量,其分别对应线段AB、BC、DE和目标曲线的移动轨迹,点在这四个轨迹上移动,才能形成动画。而要生成动画,就要用到animation的方法FuncAnimation,其含有多个参数,fig就是我们绘图的那个画布,run就是我们生成动画时运行的函数,frames是帧画面,其每一帧画面包含了这些移动轨迹中的一个点所对应的静态图,把这些点的轨迹也就是每一帧连起来就是动画轨迹,frames一般是一个sequence,也就是包含多个变量,每个变量都赋值给run函数,run函数利用这个参数生成一个静态图,这么多静态图连起来就是动画,这和我们在电影或电视中看到的动画片是一样道理。interval是帧之间的时间间隔,200代表0.2秒,这个可以随意设定。save_count=50是把帧缓存起来用于回放,缓存帧的数量越多,回放越流畅,这个影响不大,随意设定。

下面再说一下运行动画的函数run的作用,可以看到run一共有5行代码,前4行代码是绘图代码,最后一个是返回参数的代码,前4行中每一行都代表了前面我们说过的点的轨迹, art1是线段AB上的点,用set_data(x_dots12[i], y_dots12[i])方法就生成了一个对应的帧,变量i就是前面讲的frames里的一个参数,生成的这个帧返回给FuncAnimation,让其用于连续播放,这就形成了动画。后面art2、art3和art4的道理是一样的。生成的动画效果的静态截图如下:

图7. 二阶贝赛尔曲线静态成图

最后再放上一个动图,让我们在一个深V的运动中结束本次话题:

v2-74a04c398f040f307e83386be7afce86_b.jpg
图8. 二阶贝赛尔曲线动态示意图

二阶贝塞尔曲线的推导相对还容易一些,而三阶甚至更高阶的推导就复杂一点,笔者目前正在研究三阶贝赛尔曲线,以后会给大家分享一下。

完整代码已经上传:

https://gitee.com/leonmovie/two_degree_bc

有需要的可以去自行下载。

作者简介:小李子,数据分析爱好者,擅长数据可视化,比较关注机器学习领域,希望能和业内朋友多学习交流

------

欢迎搜索及关注:Crossin的编程教室

这里还有更多精彩。一起学,走得远


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK