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


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

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

 

有以下缺點:

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

 

這么厲害的算法, 大家是不是已經迫不及待了. 下面就來給大家分享這個算法的思路, 先看下面的圖解:

可能大家只看圖就已經知道應該怎么做了. 現在按照圖中的標注, 假設:ABCDEFG為原筆跡點. 

 

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

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' 作為兩個端點,

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

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塊錢的蛋炒飯, 早上點一份,中午吃一半, 晚上吃一半, 日子真實苦啊..

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

 


免責聲明!

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



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