图形学_Bezier曲线

Bezier曲线由n个控制点生成,举个例子:当n=2时,点$P_0$、$P_1$之间遵从计算:

$P_0=(1-t)P_0+tP_1$

而推广为n维时,有:

$P^n_0=(1-t)P^{n-1}_0+tP^{n-1}_1$

递推公式有:

$P^k_i=(1-t)P^{k-1}_i+tP^{k-1}_{i+1}$

 要递推生成Bezier的理论知识如上。使用py动态(…课程要求)生成曲线需要掌握matplotlib库的键鼠响应事件。

#fig.canvas.mpl_connect() 事件绑定规范用法
import matplotlib.pyplot as plt

def on_key_press(event):
    print(event.key)

fig, ax = plt.subplots()
fig.canvas.mpl_connect('key_press_event', on_key_press)
plt.show()

下面是实现过程:

定义一个Bezier类,初始化函数中保存事件状态与响应,这里就用到了事件绑定:

    def __init__(self, line):
        self.line = line
        self.index_02 = None  # 保存拖动索引
        self.press = None  # 按下
        self.pick = None  # 选中
        self.motion = None  #拖动
        self.xs = list()  # x坐标
        self.ys = list()  # y坐标
        self.cidpress = line.figure.canvas.mpl_connect('button_press_event', self.on_press)  # 鼠标按下事件
        self.cidrelease = line.figure.canvas.mpl_connect('button_release_event', self.on_release)  # 鼠标放开事件
        self.cidmotion = line.figure.canvas.mpl_connect('motion_notify_event', self.on_motion)  # 鼠标拖动事件
        self.cidpick = line.figure.canvas.mpl_connect('pick_event', self.on_picker)  # 鼠标选中事件

这里xs、ys为控制点的横、纵坐标列表。

将self.press设定为1,即表示鼠标按下被调用;

    def on_press(self, event):  # 鼠标按下调用
        if event.inaxes != self.line.axes: return
        self.press = 1

选中调用同理;

    def on_picker(self, event):  # 选中调用
        self.pick = 1

鼠标拖动事件的行为就稍微有,复杂了:

    def on_motion(self, event):  # 鼠标拖动调用
        if event.inaxes != self.line.axes: return
        if self.press is None: return
        if self.pick is None: return
        if self.motion is None:  # 按下并选中
            self.motion = 1
            x = self.xs
            xdata = event.xdata
            ydata = event.ydata
            index_01 = 0
            for i in x:
                if abs(i - xdata) < 0.02:  # 0.02 为点的半径
                    if abs(self.ys[index_01] - ydata) < 0.02: break
                index_01 = index_01 + 1
            self.index_02 = index_01
        if self.index_02 is None: return
        self.xs[self.index_02] = event.xdata  # 鼠标的坐标覆盖选中的点的坐标
        self.ys[self.index_02] = event.ydata
        self.draw_01()

一个点的按下与选中值都为1时,才可能被拖动;将self.motion设定为1后,在控制点列表中寻找被拖动的点的位置,然后更新该点的坐标。最后调用draw_01,重新构图。

draw_01如下:

    def draw_01(self):  # 绘图函数
        self.line.clear()  # 不清除的话会保留原有的图
        self.line.axis([0, 1, 0, 1])  # x和y范围0到1
        self.bezier(self.xs, self.ys)  # Bezier曲线
        self.line.scatter(self.xs, self.ys, color='b', s=200, marker="o", picker=5)  # 画点
        self.line.plot(self.xs, self.ys, color='r')  # 画线
        self.line.figure.canvas.draw()  # 重构子图

draw先清除了原本的图(不然生成的图会在原图上,生成多条曲线),使用更新过的xs、ys调用Bezier函数生成新的曲线;使用scatter作点;最后重构子图。

Bezier函数如下:

    def bezier(self, *args):  # Bezier曲线公式转换,获取x和y
        n = len(args[0])  # 点的个数
        xarray, yarray = [], []
        x, y = [], []
        index = 0
        for t in np.linspace(0, 1):
            for i in range(1, n):
                for j in range(0, n - i):
                    if i == 1:
                        xarray.insert(j, args[0][j] * (1 - t) + args[0][j + 1] * t)
                        yarray.insert(j, args[1][j] * (1 - t) + args[1][j + 1] * t)
                        continue
                    # i != 1时,通过上一次迭代的结果计算
                    xarray[j] = xarray[j] * (1 - t) + xarray[j + 1] * t
                    yarray[j] = yarray[j] * (1 - t) + yarray[j + 1] * t
            if n == 1:
                x.insert(index, args[0][0])
                y.insert(index, args[1][0])
            else:
                x.insert(index, xarray[0])
                y.insert(index, yarray[0])
                xarray = []
                yarray = []
            index = index + 1
        self.line.plot(x, y)

这里算法的"j in range(0, n-i)"范围让我疑惑了一下,个人理解是一个点不会被自己影响过的点影响;将生成的点加入x、y数组后,放入plot中。

最后是响应鼠标松开事件的函数:

    def on_release(self, event):  # 鼠标放开调用
        if event.inaxes != self.line.axes: return
        if self.pick == None:  # 如果不是选中点,那就添加点
            self.xs.append(event.xdata)
            self.ys.append(event.ydata)
        if self.pick == 1 and self.motion != 1:  # 如果是选中点,但不是拖动点,那就删去点
            x = self.xs
            xdata = event.xdata
            ydata = event.ydata
            index_01 = 0
            for i in x:
                if abs(i - xdata) < 0.02:
                    if abs(self.ys[index_01] - ydata) < 0.02: break
                index_01 = index_01 + 1
            self.xs.pop(index_01)
            self.ys.pop(index_01)
        self.draw_01()
        self.pick = None  # 所有状态恢复,鼠标按下到释放为一个周期
        self.motion = None
        self.press = None
        self.index_02 = None

 这里再次点击点被设定为将点从控制点列表中删去。函数的最后,各状态被重置。

 主函数:

fig = plt.figure(2, figsize=(12, 6))  # 创建第2个绘图对象,1200*600像素
ax = fig.add_subplot(111)  # 一行一列第一个子图
ax.set_title("Missouter's bezier")
myBezier = MyBezier(ax)
plt.xlabel('X')
plt.ylabel('Y')
plt.show()

这个程序在pycharm里不好运行的样子,cmd运行指令python demo.py。

参考博客:

https://blog.csdn.net/guofei9987/article/details/78106492

https://www.jianshu.com/p/116be2cfa708

https://blog.csdn.net/Hachi_Lin/article/details/89052318

猜你喜欢

转载自www.cnblogs.com/missouter/p/12667810.html