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的范围让我疑惑了一下,一开始写的是和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