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


之前研究過一種用於 模擬真實 手寫筆跡簽名 的算法,  要求能夠保持原筆跡平滑,並有筆鋒的效果.

在網上看了一些資料, 資料很多, 能夠達到用於正式產品中的效果的一個都沒有找到.

我看到最靠譜的一篇文章是這個:Interpolation with Bezier Curves

但是即使按照這篇文章講的方法去實現手寫筆跡, 表現的效果也非常的不理想.

而且, 這篇文章還只是涉及到了筆跡平滑的問題, 沒有涉及到如何解決筆鋒的問題

經過我一段時間的研究, 終於在上廁所的時候(有沒有被duang了一下的感覺, 哈哈~O(∩_∩)O), 想出來了一種方法..先給大家展示兩張在正式產品中的效果圖:

前面兩張圖片是在手機上測試的效果,后面兩張是在電腦上用鼠標寫出來的效果.

 

當然, 必須承認, 圖片中展示的效果效果的文字, 我反復寫了很多次...隨便畫幾條線大概是這樣:

我將要介紹的這種算法, 還可以通過對某些參數的修改,  模擬出毛筆, 鋼筆, 簽字筆等各種筆...真實書寫效果....

 

如果你還對貝塞爾曲線不了解, 我推薦查看這篇文章:史上最全的貝塞爾曲線(Bezier)全解,   所以, 在這里我會假設讀者已經對Bezier曲線已經比較了解.

本文主要講解 如何通過已知所有筆跡點, 計算出控制點, 使用3次bezier曲線擬合筆跡, 達到筆跡平滑的效果, 解決筆跡平滑的問題,.

除了本篇文章意外, 后面應該還會有兩篇文章:

第二篇:介紹自己開發的一種筆跡擬合算法.

第三篇:主要介紹實現筆鋒的效果.並提供最終的c++對此算法的實現的源代碼和演示程序.

 Bezier曲線是通過簡單地指定端點中間的控制點(Control Point)來描繪出一條光滑的曲線, 三次貝塞爾曲線的效果是圖片中這樣:

當紅色的圓點代表原筆跡點時, 想必大家想要的效果是下面圖片中的藍色線條, 而不是紅色線條吧:

貝賽爾曲線擬合會經過前后兩個端點, 但不會經過中間的控制點,所以, 我們通過貝塞爾曲線來擬合筆跡點的時候, 是要:

對於所有的筆跡點, 每相鄰的一對筆跡點作為前后端點來繪制Bezier曲線, 所有我們需要找出一些滿足某種規律的點作為這些端點中間的控制點.

下面請看下圖:

圖中, 點A, B, C為我們的原筆跡點, B' 和 B''為我們計算出來的控制點.

計算控制點的方法是:

1) 設定一個0到1的系數k,  在AB和BC上找到兩點, b'和c', 使得距離比值, Bb' / AB = Bc' / BC = k  , 計算出兩個點 b' 和 c'..(k的大小決定控制點的位置,最終決定筆跡的平滑程度, k越小, 筆跡越銳利; k越大,則筆跡越平滑.)

2) 然后在b' c'這條線段上再找到一個點 t, 且線段的長度滿足比例: b't / tc' = AB / BC,

3) 把b' 和 c', 沿着 點 t 到 點B的方向移動, 直到 t 和 B重合. 由b'移動后得到 B', 由 c'移動后的距離得到B'', B'和B''就是我們要計算的位於頂點B附近的兩個控制點.

實際項目過程中, 使用下面的規則進行繪制筆跡:

1) 當我們在手寫原筆跡繪制的時候, 得到第3個點(假設分別為ABC)的時候, 可以計算出B點附近的兩個控制點., 由於是點A為起始點,, 所以直接把點A作為第一個控制點, 計算出來的B'作為第二個控制點,  這樣AAB'B 4個點,就可以畫出點A到點B的平滑貝塞爾曲線.(或者可以直接把AB'B這3個點, 把B'作為控制點, 用二次貝塞爾曲線來擬合, 也是可以的哦~.)

2) 當得到第4個點(假設為D)的時候, 我們通過BCD, 計算出在點C附近的兩個控制點, C'和C'', 通過BB''C'C繪制出B到C的平滑曲線..

3) 當得到第i個點的時候, 進行第2個步驟.........

4) 當得到最后一個點Z的時候,  直接把Z作為第二個控制點(假設前一個點為Y),  即, 使用YY'ZZ來繪制Bezier曲線.

為了讓閱讀者能夠更好的理解, 用Python實現了這個算法, 鼠標點擊空白處可以增加筆跡點, 選中筆跡點可以動態拖動, 單擊已有筆跡點執行刪除:

效果圖如下:

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 
 25         self.ctl_point_1 = None
 26 
 27     def on_press(self, event):  # 鼠標按下調用
 28         if event.inaxes != self.line.axes: return
 29         self.press = 1
 30 
 31     def on_motion(self, event):  # 鼠標拖動調用
 32         if event.inaxes != self.line.axes: return
 33         if self.press is None: return
 34         if self.pick is None: return
 35         if self.motion is None:  # 整個if獲取鼠標選中的點是哪個點
 36             self.motion = 1
 37             x = self.xs
 38             xdata = event.xdata
 39             ydata = event.ydata
 40             index_01 = 0
 41             for i in x:
 42                 if abs(i - xdata) < 0.02:  # 0.02 為點的半徑
 43                     if abs(self.ys[index_01] - ydata) < 0.02: break
 44                 index_01 = index_01 + 1
 45             self.index_02 = index_01
 46         if self.index_02 is None: return
 47         self.xs[self.index_02] = event.xdata  # 鼠標的坐標覆蓋選中的點的坐標
 48         self.ys[self.index_02] = event.ydata
 49         self.draw_01()
 50 
 51     def on_release(self, event):  # 鼠標按下調用
 52         if event.inaxes != self.line.axes: return
 53         if self.pick is None:  # 如果不是選中點,那就添加點
 54             self.xs.append(event.xdata)
 55             self.ys.append(event.ydata)
 56         if self.pick == 1 and self.motion != 1:  # 如果是選中點,但不是拖動點,那就降階
 57             x = self.xs
 58             xdata = event.xdata
 59             ydata = event.ydata
 60             index_01 = 0
 61             for i in x:
 62                 if abs(i - xdata) < 0.02:
 63                     if abs(self.ys[index_01] - ydata) < 0.02: break
 64                 index_01 = index_01 + 1
 65             self.xs.pop(index_01)
 66             self.ys.pop(index_01)
 67         self.draw_01()
 68         self.pick = None  # 所有狀態恢復,鼠標按下到稀放為一個周期
 69         self.motion = None
 70         self.press = None
 71         self.index_02 = None
 72 
 73     def on_picker(self, event):  # 選中調用
 74         self.pick = 1
 75 
 76     def draw_01(self):  # 繪圖
 77         self.line.clear()  # 不清除的話會保留原有的圖
 78         self.line.set_title('Bezier曲線擬合手寫筆跡')
 79         self.line.axis([0, 1, 0, 1])  # x和y范圍0到1
 80         # self.bezier(self.xs, self.ys)  # Bezier曲線
 81         self.all_curve(self.xs, self.ys)
 82         self.line.scatter(self.xs, self.ys, color='b', s=20, marker="o", picker=5)  # 畫點
 83         self.line.plot(self.xs, self.ys, color='black', lw=0.5)  # 畫線
 84         self.line.figure.canvas.draw()  # 重構子圖
 85 
 86     # def list_minus(self, a, b):
 87     #     list(map(lambda x, y: x - y, middle, begin))
 88 
 89     def controls(self, k, begin, middle, end):
 90         # if k > 0.5 or k <= 0:
 91         #     print('value k not invalid, return!')
 92         #     return
 93 
 94         diff1 = middle - begin
 95         diff2 = end - middle
 96 
 97         l1 = (diff1[0] ** 2 + diff1[1] ** 2) ** (1 / 2)
 98         l2 = (diff2[0] ** 2 + diff2[1] ** 2) ** (1 / 2)
 99 
100         first = middle - (k * diff1)
101         second = middle + (k * diff2)
102 
103         c = first + (second - first) * (l1 / (l2 + l1))
104 
105         # self.line.text(begin[0] - 0.2, begin[1] + 1.5, 'A', fontsize=12, verticalalignment="top",
106         #                horizontalalignment="left")
107         # self.line.text(middle[0] - 0.2, middle[1] + 1.5, 'B', fontsize=12, verticalalignment="top",
108         #                horizontalalignment="left")
109         # self.line.text(end[0] + 0.2, end[1] + 1.5, 'C', fontsize=12, verticalalignment="top",
110         #                horizontalalignment="left")
111         # xytext = [(first[0] + second[0]) / 2, min(first[1], second[1]) - 10]
112         #
113         arrow_props = dict(arrowstyle="<-", connectionstyle="arc3")
114         # self.line.annotate('', first, xytext=xytext, arrowprops=dict(arrowstyle="<-", connectionstyle="arc3,rad=-.1"))
115         # self.line.annotate('', c, xytext=xytext, arrowprops=arrow_props)
116         # self.line.annotate('', second, xytext=xytext, arrowprops=dict(arrowstyle="<-", connectionstyle="arc3,rad=.1"))
117 
118         # label = '從左到右3個點依次分別為b\', c\', t,\n' \
119         #         '滿足條件 k = |b\'B| / |AB|, k = |c\'B| / |CB|\n' \
120         #         '然后把線段(b\'c\')按 t 到 B的路徑移動,\n' \
121         #         '最后得到的兩個端點就是我們要求的以B為頂點的控制點'
122         # self.line.text(xytext[0], xytext[1], label, verticalalignment="top", horizontalalignment="center")
123         self.line.plot([first[0], c[0], second[0]], [first[1], c[1], second[1]], linestyle='dashed', color='violet', marker='o', lw=0.3)
124 
125         first_control = first + middle - c
126         second_control = second + middle - c
127 
128         # self.line.text(first_control[0] - 0.2, first_control[1] + 1.5, '控制點B\'', fontsize=9, verticalalignment="top",
129         #                horizontalalignment="left")
130         # self.line.text(second_control[0] + 0.2, second_control[1] + 1.5, '控制點B\'\'', fontsize=9,
131         #                verticalalignment="top", horizontalalignment="left")
132         x_s = [first_control[0], second_control[0]]
133         y_s = [first_control[1], second_control[1]]
134 
135         # self.line.annotate('', xy=middle, xytext=c, arrowprops=dict(facecolor='b' headlength=10, headwidth=25, width=20))
136         arrow_props['facecolor'] = 'blue'
137         # arrow_props['headlength'] = 5
138         # arrow_props['headwidth'] = 10
139         # arrow_props['width'] = 5
140         # self.line.annotate('', xy=c, xytext=middle, arrowprops=arrow_props)
141         # self.line.annotate('', xy=first, xytext=first_control, arrowprops=arrow_props)
142         # self.line.annotate('', xy=second, xytext=second_control, arrowprops=arrow_props)
143         # self.line.plot([begin[0], middle[0], end[0]], [begin[1], middle[1], end[1]], lw=1.0, marker='o')
144         self.line.plot(x_s, y_s, marker='o', lw=1, color='r', linestyle='dashed')
145         # self.line.plot(x_s, y_s, lw=1.0)
146 
147         return first_control, second_control
148 
149     def all_curve(self, xs, ys):
150         self.ctl_point_1 = None
151         le = len(xs)
152         if le < 3: return
153 
154         begin = [xs[0], ys[0]]
155         middle = [xs[1], ys[1]]
156         end = [xs[2], ys[2]]
157         self.one_curve(begin, middle, end)
158 
159         for i in range(3, le):
160             begin = middle
161             middle = end
162             end = [xs[i], ys[i]]
163             self.one_curve(begin, middle, end)
164 
165         end = [xs[le - 1], ys[le - 1]]
166         x = [middle[0], self.ctl_point_1[0], end[0]]
167         y = [middle[1], self.ctl_point_1[1], end[1]]
168         self.bezier(x, y)
169 
170     def one_curve(self, begin, middle, end):
171         ctl_point1 = self.ctl_point_1
172 
173         begin = np.array(begin)
174         middle = np.array(middle)
175         end = np.array(end)
176 
177         ctl_point2, self.ctl_point_1 = self.controls(0.3, np.array(begin), np.array(middle), np.array(end))
178         if ctl_point1 is None: ctl_point1 = begin
179 
180         xs = [begin[0], ctl_point1[0], ctl_point2[0], middle[0]]
181         ys = [begin[1], ctl_point1[1], ctl_point2[1], middle[1]]
182         self.bezier(xs, ys)
183 
184         # xs = [middle[0], self.ctl_point_1[0], end[0], end[0]]
185         # ys = [middle[1], self.ctl_point_1[1], end[1], end[1]]
186         # self.bezier(xs, ys)
187 
188     def bezier(self, *args):  # Bezier曲線公式轉換,獲取x和y
189         t = np.linspace(0, 1)  # t 范圍0到1
190         le = len(args[0]) - 1
191 
192         self.line.plot(args[0], args[1], marker='o', color='r', lw=0.8)
193         le_1 = 0
194         b_x, b_y = 0, 0
195         for x in args[0]:
196             b_x = b_x + x * (t ** le_1) * ((1 - t) ** le) * comb(len(args[0]) - 1, le_1)  # comb 組合,perm 排列
197             le = le - 1
198             le_1 = le_1 + 1
199 
200         le = len(args[0]) - 1
201         le_1 = 0
202         for y in args[1]:
203             b_y = b_y + y * (t ** le_1) * ((1 - t) ** le) * comb(len(args[0]) - 1, le_1)
204             le = le - 1
205             le_1 = le_1 + 1
206 
207         color = "yellowgreen"
208         if len(args) > 2 : color = args[2]
209         self.line.plot(b_x, b_y, color=color, linewidth='3')
210 
211 fig = plt.figure(2, figsize=(12, 6))
212 ax = fig.add_subplot(111)  # 一行一列第一個子圖
213 ax.set_title('手寫筆跡貝賽爾曲線, 計算控制點圖解')
214 
215 handwriting = Handwriting(ax)
216 plt.xlabel('X')
217 plt.ylabel('Y')
218 
219 # begin = np.array([20, 6])
220 # middle = np.array([30, 40])
221 # end = np.array([35, 4])
222 # handwriting.one_curve(begin, middle, end)
223 # handwriting.controls(0.2, begin, middle, end)
224 plt.show()

大家可能覺得這個算法已經比較完美了, 下面我指出這種算法在實際使用中, 幾個問題,  其中一些讓人完全不能接受:

1) 在實際交互過程中, 這種方法需要3次貝塞爾曲線來擬合, 用戶輸入完第3個點,才能繪制第一條曲線, 第4個點才能繪制第2條曲線, 這種反饋不及時, 讓體驗非常差.

2) 每次都要計算控制點, 非常麻煩, 並且還影響效率.

在下一篇文章中, 我會介紹自己實現的解決了這些缺點的一種算法.

另外, 吐槽一下啊, 公司老板拖欠兩個月工資了,  窮得叮當響, 每天吃8塊錢的蛋炒飯.真尼瑪坑啊,我靠!!!!!!!!

大家如果大家覺得這篇文章對您有幫助, 又願意打賞一些銀兩, 請拿起你的手機, 打開你的微信, 掃一掃下方二維碼, 作為一個有骨氣的程序員攻城獅, 我非常願意接受大家的支助...哈哈哈!!!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM