原筆跡手寫實現平滑和筆鋒效果之:筆跡的平滑(二)

上一篇文章介紹了目前大多數人在擬合手寫筆跡的時候使用的算法, 這篇文章介紹一種本身首創的算法.html

這種算法具備如下優勢:
1) 使用二次貝塞爾曲線擬合, 計算量大概比3次貝塞爾曲線少三分之一.
2) 沒必要等到用戶輸入了下一個點以後, 才能繪製當前兩個點之間的曲線, 這種算法能夠先繪當前須要擬合的線段的一部分, 可以很是及時的把用戶的輸入反饋給用戶, 用戶體驗馬上提升了2個檔次.
3) 不用計算控制點, 處理起來更加簡單, 計算量也再次減小, 用戶繪製體驗獲得進一步提升.
4) 筆跡擬合更加接近真實手寫的筆跡.python

 

有如下缺點:程序員

我真尼瑪沒發現有缺點, 我真的不能欺騙你們, 它明明沒有缺點, 我非要找一個缺點出來嗎!!!?,做爲一個程序員, 我不能說謊啊!!!!!O(∩_∩)O哈哈~算法

 

這麼厲害的算法, 你們是否是已經火燒眉毛了. 下面就來給你們分享這個算法的思路, 先看下面的圖解:canvas

可能你們只看圖就已經知道應該怎麼作了. 如今按照圖中的標註, 假設:ABCDEFG爲原筆跡點. 微信

 

1) 當用戶經過點擊鼠標或者點擊手機屏幕手勢, 輸入點A時, 咱們在A的位置畫下一個小圓點app

2) 首先須要設立一個係數k,取值爲(0, 0.5]之間的小數. 當用於經過移動, 輸入了第二個點B時, 咱們在線段AB上找到一個點A', 使得 |A'B| / |AB| = k, 並繪製線段AA', 將其做爲手寫筆跡的一部分. 學習

3) 當用戶再次移動鼠標, 獲得獲得第三個點C時, 咱們在BC上, 找到兩個點, B' 和 B'', 知足 |BB'| / |BC| = |B''C| / |BC| = k, 而後將前面的 A' 和 B' 做爲兩個端點, spa

  點B做爲控制點, 繪製A'BB' 描述的二次貝塞爾曲線. 做爲手寫筆跡的一部分.code

4) 鏈接B'B''的直線線段, 做爲時候寫筆跡的一部分. 

5) 當用於輸入點D,E,F.......時, 回到第2步, 循環執行2,3,4.

6) 當用於輸入最後一個點G時, 執行2, 3步, 而後直接鏈接F'G, 結束繪製.

 

爲何要把第4步單獨分離出來呢, 由於當k取值爲0.5的時候, B'B'', C'C''.....F'F'' 直接重合爲同一個點, 就能夠直接省略弟4步.(實踐證實, k值取0.5, 不但速度快, 效果還很是好!!!!)

 

這個算法, 初看起來, 有一些問題, 整個曲線沒有通過做爲原筆跡點的BCDEF, 是否是效果不理想呢???..再細想一下:

使用點ABC來舉例, 雖然沒有通過點B, AA'和B'B兩條線段的軌跡是徹底和原筆跡的連線重合的, 即便閾值取0.5的狀況, 也有兩個點(A', B')和原筆跡連線重合'

因此, 咱們雖然放棄了一棵樹,獲得了一片森林;放棄一個點, 重合了無數個點, 咱們還能夠經過閾值k來控制曲線的擬合程度, k越小, 轉角的地方越銳利; k越大, 擬合越平滑.

 

一樣,爲了你們學習方便, 我在前面一篇文章的基礎上稍做修改, 把這種算法用Python實現出來, 提供你們參考和理解:

  1 #!/usr/bin/env python
  2 # -*- coding: utf-8 -*-
  3 import numpy as np
  4 from scipy.special import comb, perm
  5 import matplotlib.pyplot as plt
  6 
  7 plt.rcParams['font.sans-serif'] = ['SimHei']
  8 # plt.rcParams['font.sans-serif'] = ['STXIHEI']
  9 plt.rcParams['axes.unicode_minus'] = False
 10 
 11 class Handwriting:
 12     def __init__(self, line):
 13         self.line = line
 14         self.index_02 = None  # 保存拖動的這個點的索引
 15         self.press = None  # 狀態標識,1爲按下,None爲沒按下
 16         self.pick = None  # 狀態標識,1爲選中點並按下,None爲沒選中
 17         self.motion = None  # 狀態標識,1爲進入拖動,None爲不拖動
 18         self.xs = list()  # 保存點的x座標
 19         self.ys = list()  # 保存點的y座標
 20         self.cidpress = line.figure.canvas.mpl_connect('button_press_event', self.on_press)  # 鼠標按下事件
 21         self.cidrelease = line.figure.canvas.mpl_connect('button_release_event', self.on_release)  # 鼠標放開事件
 22         self.cidmotion = line.figure.canvas.mpl_connect('motion_notify_event', self.on_motion)  # 鼠標拖動事件
 23         self.cidpick = line.figure.canvas.mpl_connect('pick_event', self.on_picker)  # 鼠標選中事件
 24         self.ctl_point_1 = None
 25 
 26     def on_press(self, event):  # 鼠標按下調用
 27         if event.inaxes != self.line.axes: return
 28         self.press = 1
 29 
 30     def on_motion(self, event):  # 鼠標拖動調用
 31         if event.inaxes != self.line.axes: return
 32         if self.press is None: return
 33         if self.pick is None: return
 34         if self.motion is None:  # 整個if獲取鼠標選中的點是哪一個點
 35             self.motion = 1
 36             x = self.xs
 37             xdata = event.xdata
 38             ydata = event.ydata
 39             index_01 = 0
 40             for i in x:
 41                 if abs(i - xdata) < 0.02:  # 0.02 爲點的半徑
 42                     if abs(self.ys[index_01] - ydata) < 0.02: break
 43                 index_01 = index_01 + 1
 44             self.index_02 = index_01
 45         if self.index_02 is None: return
 46         self.xs[self.index_02] = event.xdata  # 鼠標的座標覆蓋選中的點的座標
 47         self.ys[self.index_02] = event.ydata
 48         self.draw_01()
 49 
 50     def on_release(self, event):  # 鼠標按下調用
 51         if event.inaxes != self.line.axes: return
 52         if self.pick is None:  # 若是不是選中點,那就添加點
 53             self.xs.append(event.xdata)
 54             self.ys.append(event.ydata)
 55         if self.pick == 1 and self.motion != 1:  # 若是是選中點,但不是拖動點,那就降階
 56             x = self.xs
 57             xdata = event.xdata
 58             ydata = event.ydata
 59             index_01 = 0
 60             for i in x:
 61                 if abs(i - xdata) < 0.02:
 62                     if abs(self.ys[index_01] - ydata) < 0.02: break
 63                 index_01 = index_01 + 1
 64             self.xs.pop(index_01)
 65             self.ys.pop(index_01)
 66         self.draw_01()
 67         self.pick = None  # 全部狀態恢復,鼠標按下到稀放爲一個週期
 68         self.motion = None
 69         self.press = None
 70         self.index_02 = None
 71 
 72     def on_picker(self, event):  # 選中調用
 73         self.pick = 1
 74 
 75     def draw_01(self):  # 繪圖
 76         self.line.clear()  # 不清除的話會保留原有的圖
 77         self.line.set_title('Bezier曲線擬合手寫筆跡')
 78         self.line.axis([0, 1, 0, 1])  # x和y範圍0到1
 79         # self.bezier(self.xs, self.ys)  # Bezier曲線
 80         self.all_curve(self.xs, self.ys)
 81         self.line.scatter(self.xs, self.ys, color='b', s=20, marker="o", picker=5)  # 畫點
 82         # self.line.plot(self.xs, self.ys, color='black', lw=0.5)  # 畫線
 83         self.line.figure.canvas.draw()  # 重構子圖
 84 
 85     # def list_minus(self, a, b):
 86     #     list(map(lambda x, y: x - y, middle, begin))
 87 
 88     def controls(self, k, begin, end):
 89         if k <= 0 or k >= 1: return
 90         first_middle = begin + k * (end - begin)
 91         second_middle = begin + (1 - k) * (end - begin)
 92         return first_middle, second_middle
 93 
 94 
 95     def all_curve(self, xs, ys):
 96         le = len(xs)
 97         if le < 2: return
 98         self.ctl_point_1 = None
 99 
100         begin = [xs[0], ys[0]]
101         end = [xs[1], ys[1]]
102         self.one_curve(begin, end)
103 
104         for i in range(2, le):
105             begin = end
106             end = [xs[i], ys[i]]
107             self.one_curve(begin, end)
108 
109         end = [xs[le - 1], ys[le - 1]]
110         x = [self.ctl_point_1[0], end[0]]
111         y = [self.ctl_point_1[1], end[1]]
112 
113         #linestyle='dashed',
114         self.line.plot(x, y,  color='yellowgreen', marker='o', lw=3)
115 
116     def one_curve(self, begin, end):
117         ctl_point1 = self.ctl_point_1
118 
119         begin = np.array(begin)
120         end = np.array(end)
121 
122         ctl_point2, self.ctl_point_1 = self.controls(0.4, begin, end)
123         color = 'red';
124         if ctl_point1 is None :
125             xs = [begin[0], self.ctl_point_1[0]]
126             ys = [begin[1], self.ctl_point_1[1]]
127             self.line.plot(xs, ys, color=color, marker='o', linewidth='3')
128         else :
129             xs = [ctl_point1[0], begin[0], ctl_point2[0]]
130             ys = [ctl_point1[1], begin[1], ctl_point2[1]]
131             self.bezier(xs, ys)
132             xs = [ctl_point2[0], self.ctl_point_1[0]]
133             ys = [ctl_point2[1], self.ctl_point_1[1]]
134             self.line.plot(xs, ys, color=color, marker='o', linewidth='3')
135 
136     def bezier(self, *args):  # Bezier曲線公式轉換,獲取x和y
137         t = np.linspace(0, 1)  # t 範圍0到1
138         le = len(args[0]) - 1
139 
140         self.line.plot(args[0], args[1], marker='o', linestyle='dashed', color='limegreen', lw=1)
141         le_1 = 0
142         b_x, b_y = 0, 0
143         for x in args[0]:
144             b_x = b_x + x * (t ** le_1) * ((1 - t) ** le) * comb(len(args[0]) - 1, le_1)  # comb 組合,perm 排列
145             le = le - 1
146             le_1 = le_1 + 1
147 
148         le = len(args[0]) - 1
149         le_1 = 0
150         for y in args[1]:
151             b_y = b_y + y * (t ** le_1) * ((1 - t) ** le) * comb(len(args[0]) - 1, le_1)
152             le = le - 1
153             le_1 = le_1 + 1
154 
155         color = "mediumseagreen"
156         if len(args) > 2: color = args[2]
157         self.line.plot(b_x, b_y, color=color, linewidth='3')
158 
159 fig = plt.figure(2, figsize=(12, 6))
160 ax = fig.add_subplot(111)  # 一行一列第一個子圖
161 ax.set_title('手寫筆跡貝賽爾曲線, 計算控制點圖解')
162 
163 handwriting = Handwriting(ax)
164 plt.xlabel('X')
165 plt.ylabel('Y')
166 
167 # begin = np.array([20, 6])
168 # middle = np.array([30, 40])
169 # end = np.array([35, 4])
170 # handwriting.one_curve(begin, middle, end)
171 # myBezier.controls(0.2, begin, middle, end)
172 plt.show()

下一篇文章,不出意外應該是這個手寫筆跡系列的最後一篇文章.

我將把我實現筆鋒效果的具體原理和細節, 還有用C++對算法的具體實現, 以及能夠直接運行查看效果的Demo一塊兒分享給你們. 

 

無良公司老闆拖欠兩個月工資了,  窮得叮噹響, .真尼瑪坑啊,我靠!!!!!!!!如今天天吃8塊錢的蛋炒飯, 早上點一份,中午吃一半, 晚上吃一半, 日子真實苦啊..

你們若是你們以爲這篇文章對您有幫助, 又願意打賞一些銀兩, 請拿起你的手機, 打開你的微信, 掃一掃下方二維碼, 做爲一個有骨氣的程序員攻城獅, 我很是願意接受你們的支助...哈哈哈!!!

 

相關文章
相關標籤/搜索