563 |
564 |

565 |
566 |
567 | ملخص
568 |
569 |
570 | يعد الخط جزءًا أساسيًا من التراث والثقافة العربية. وقد استخدم قديماً لتزيين المنازل والمساجد. عادةً ما يتم تصميم هذا الخط يدويًا بواسطة خبراء يتمتعون برؤى جمالية. في السنوات القليلة الماضية ، كان هناك جهد كبير لرقمنة هذا النوع من الفن إما عن طريق التقاط صورة للمباني المزخرفة أو رسمها باستخدام الأجهزة الرقمية. يعتبر هذا الأخير نموذجًا عبر الإنترنت حيث يتم تتبع الرسم عن طريق تسجيل حركة الجهاز ، مثل القلم الإلكتروني ، على الشاشة. في البحوث المنشورة ، هناك العديد من مجموعات البيانات لرقمنة الخطوط العربية التي تم جمعها مع مجموعة متنوعة من أنماط الخط العربي. ومع ذلك ، لا توجد مجموعة بيانات متاحة لتتبع الرسم للخط العربي. في هذه الورقة، نوضح منهجنا في جمع مجموعة بيانات عبر الإنترنت للخط العربي تسمى كاليار
571 | . تم جمع 2500 صورة وتوسيمها بالرسم ليمكن اتمتة العملية بسهولة تامة في المستقبل.
572 |
573 |
574 |
575 |
576 | تجربة
577 |
578 |
579 | يمكنك إنشاء رسم متحرك لمجموعة صغيرة من الملفات من مجموعة البيانات بالنقر فوق الزر أدناه. لاحظ أن هذا يعمل مباشرة في المتصفح ومن ثم استخدمنا 100 عينة فقط للتوضيح. تم تحويل 100 ملف json إلى إصدار مصغر حيث تم تحويل جميع النقاط إلى أعداد صحيحة . عند كل ضغطة على إنشاء ، يتم تقديم رسم جديد حيث يتم رسم كل حد بلون مختلف.
580 |
581 |
582 |
583 |
584 |
585 |
586 |
587 |
590 |
591 |
592 |
593 | توسيم البيانات
594 |
595 |
596 |
597 | تم إجراء عملية جمع البيانات خلال عدة أشهر. استخدمنا بعض الصور التي جمعناها لإنشاء الخط بشكل اتوماتيكي كما هو موضح في الصورة أدناه. أحد المواقع الرئيسية التي استخدمناها في البداية هو موقع الأسماء العربية. لحسن الحظ ، كانت مجموعة البيانات هذه تحتوي على بعض التعليقات التوضيحية النصية وكان علينا فقط قضاء بضع ساعات لتوسميها برسم بضع مئات من الصور. بالنسبة لمجموعات البيانات الأخرى ، كان علينا أن نوسم الصور بالنص ثم بالرسم التخطيطي. استغرق إجراء التعليق التوضيحي بضعة أسابيع. استخدمنا جهازي تابليت لوحي Samsung Galaxy Tab S6 كأجهزة رئيسية لإجراء التعليقات التوضيحية. استغرق الأمر مرحلتين رئيسيتين: الأولى تعلق على كل صورة بنص. في المرحلة الثانية ، استخدمنا قلم الكمبيوتر اللوحي لرسم التعليق التوضيحي. كررنا هذه الخطوات عدة مرات لزيادة حجم مجموعة البيانات. نظرًا لأن مجموعة البيانات الأولية كانت 600 × 600 ، فقد قررنا تحويل البعد الأقصى إلى 600 وإعادة قياس البعد الآخر وفقًا لذلك للحفاظ على نسبة أبعاد الصورة. أثناء عملية التعليقات التوضيحية ، واجهنا العديد من المشكلات مثل التعليقات التوضيحية الفارغة ، الرسوم متكررة ومشاكل في شاشة اللمس أدت إلى إنشاء تعليقات توضيحية خاطئة. للتعامل مع هذه المشاكل ، اتخذنا الكثير من خطوات التحقق بشكل دوري عن طريق رسمها بواسطة بايثون. في بعض الأوقات نضع رسومات على بعض الصور ثم نزيلها لأن لديهم بعض المشاكل. أحد عمليات التحقق من الصحة المستخدمة هو مقارنة نص الصورة مع وسم الحروف المختلفة . هذا يعطينا فكرة سريعة عن الأخطاء الشائعة. من الصعب التعامل مع المشاكل الأخرى خاصة مشكلة وجود رسومات خاطئة على الجهاز اللوحي. لقد اعتمدنا على معالجة الملفات المحفوظة على شكل json لتتخلص من هذه المشاكل.
598 |
599 |

600 |
601 |
602 |
603 |
604 | التطبيقات الذكية
605 |
606 |
607 | هناك الكثير من الجدل في مجال الذكاء الاصطناعي حول ما إذا كان بإمكاننا استخدامه لإنشاء بعض البرامج الذكية والفنية. في السنوات القليلة الماضية ، كان هناك الكثير من الأبحاث في تطبيق الذكاء الاصطناعي بشكل إبداعي مثل Style Transfer [1] و Deep Dream [2] و GauGAN [3] وما إلى ذلك. تنطبق معظم هذه التقنيات على الصور. من ناحية أخرى ، فهي أكثر صعوبة في معالجة اللغة الطبيعية (NLP). تكمن الصعوبة الجوهرية في تعقيد لغة النمذجة ، ناهيك عن إنشاء بعض التطبيقات الإبداعية. يعد إنشاء الرسومات أحد أكثر التطبيقات إثارة للاهتمام. واحدة من أكثر الأوراق إثارة للاهتمام هي ورقة Alex Gravesتوليد تسلسلات مع الشبكات العصبية المتكررة . تفترض الورقة وجود مجموعة بيانات موسمة بالرسم لتوليد الخطوط باللغة الإنجليزية. بناءً على ذلك ، كان هناك العديد من الأوراق في هذا المجال مثل sketchRNN [4] و GANwriting [5] و Scrabble-GANs [6] و DF-GANs [7] و BézierSketch [8] و DoodlerGAN [9]. معظم هذه الأوراق مخصصة للبيانات باللغة الإنجليزية ولا داعي لذكر مدى بساطة اللغة الإنجليزية مقارنة باللغات الأخرى مثل العربية. ينشأ تعقيد اللغة العربية من الطبيعة المتصلة للحروف. ناهيك عن التاريخ الطويل للخط العربي الذي يستخدم على نطاق واسع في الوقت الحاضر. تترافق الأنماط المختلفة للخط العربي مع حرية رسم بعض الحروف مما يجعل المشكلة أكثر صعوبة. نظرًا لكون كاليار مجموعة البيانات الوحيدة التي تجمع معلومات الرسوم للخطوط العربية لأنماط الخط المختلفة ، فإن هذا يفتح الباب للعديد من التطبيقات. لا تزال الصعوبة تكمن في نمذجة اللغة الطبيعية من وإنشاء خطوط يمكن فهمها . هناك مفاضلة بين إنشاء خطوط جميلة وتوليد لغة منطقية.
608 |
609 |
610 | منشور التويتر
611 |
612 |
613 |
614 |
615 |
616 | فيديو
617 |
618 |
619 |
620 |
624 |
625 |
626 |
627 | الفيديو 1 أ:
628 |
629 | تحريك أنماط الخط المختلفة
630 |
631 |
632 |
633 |
634 |
635 |
639 |
640 |
641 |
642 | الفيديو 1 ب:
643 |
644 | تحريك أنماط الخط المختلفة
645 |
646 |
647 |
648 |
649 |
650 |
654 |
655 |
656 |
657 | فيديو 3:
658 |
659 | تحريك نفس العبارة باستخدام أنماط متعددة
660 |
661 |
662 |
663 |
664 |
665 |
666 |
667 |
671 |
672 |
673 |
674 | الفيديو 4:
675 |
676 | تحريك الأنماط الخطية المعقدة
677 |
678 |
679 |
680 |
681 |
682 | @misc{alyafeai2021calliar,
683 | title={Calliar: An Online Handwritten Dataset for Arabic Calligraphy},
684 | author={Zaid Alyafeai and Maged S. Al-shaibani and Mustafa Ghaleb and Yousif Ahmed Al-Wajih},
685 | year={2021},
686 | eprint={2106.10745},
687 | archivePrefix={arXiv},
688 | primaryClass={cs.CL}
689 | }
690 |
691 |
692 | المصادر
693 |
694 |
695 | - Johnson, Justin, Alexandre Alahi, and Li Fei-Fei. "Perceptual losses for real-time style transfer and super-resolution." European conference on computer vision. Springer, Cham, 2016.
696 | - Mordvintsev, Alexander, Christopher Olah, and Mike Tyka. "Inceptionism: Going deeper into neural networks." (2015).
697 | - Park, Taesung, et al. "GauGAN: semantic image synthesis with spatially adaptive normalization." ACM SIGGRAPH 2019 Real-Time Live!. 2019. 1-1.
698 | - Ha, David, and Douglas Eck. "A neural representation of sketch drawings." arXiv preprint arXiv:1704.03477 (2017).
699 | - Kang, Lei, et al. "GANwriting: Content-conditioned generation of styled handwritten word images." European Conference on Computer Vision. Springer, Cham, 2020.
700 | - Fogel, Sharon, et al. "ScrabbleGAN: semi-supervised varying length handwritten text generation." Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2020.
701 | - Tao, Ming, et al. "Df-gan: Deep fusion generative adversarial networks for text-to-image synthesis." arXiv preprint arXiv:2008.05865 (2020).
702 | - Das, Ayan, et al. "BézierSketch: A generative model for scalable vector sketches." arXiv preprint arXiv:2007.02190 (2020).
703 | - Ge, Songwei, et al. "Creative Sketch Generation." arXiv preprint arXiv:2011.10039 (2020).
704 |
705 |
706 |
707 |
708 |
711 |
712 |
713 |
714 |
715 |
716 |
717 |
718 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | cairosvg
2 | svgwrite
3 | rdp
4 | matplotlib
5 | numpy
6 | tqdm
7 | django~=3.2
8 | backcall==0.2.0
--------------------------------------------------------------------------------
/scripts/chars.py:
--------------------------------------------------------------------------------
1 | #https:#jrgraphix.net/r/Unicode/0600-06FF
2 | map_chars = {
3 | "\u0623":["\u0621", "\u0627"], # أ
4 | "\u0622":["\u0605", "\u0627"], # آ
5 | "\u0625":["\u0627", "\u0621"], # إ
6 | "\u0628":["\u066E", "."], # ب
7 | "\u062A":[".", ".", "\u066E"], # ت
8 | "\u062B":[".", ".", ".", "\u066E"], # ث
9 | "\u062C":["\u062D", "."], # ج
10 | "\u062E":[".", "\u062D"], # خ
11 | "\u0630":[".", "\u062F"], # ذ
12 | "\u0632":[".", "\u0631"], # ز
13 | "\u0634":[".", ".", ".", "\u0633"], # ش
14 | "\u0636":[".", "\u0635"], # ض
15 | "\u0637":["\u0627", "\uFEBB"], # ط
16 | "\u0638":[".", "\u0627", "\uFEBB"], # ظ
17 | "\u063A":[".", "\u0639"], # غ
18 | "\u0641":[".", "\u066F"], # ف
19 | "\u0642":[".", ".", "\u066F"], # ق
20 | "\u06A4":[".", ".", ".", "\u066F"], # ڤ
21 | "\u0643":["\u0621", "\u0644"], # ك
22 | "\u0646":[".", "\u06BA"], # ن
23 | "\u0624":["\u0621", "\u0648"], # ؤ
24 | "\u064A":["\u0649", ".", "."], #ي
25 | "\u0626":["\u0621", "\u0649"], #ئ
26 | "\u0629":[".", ".", "\u0647"], #ه
27 | }
--------------------------------------------------------------------------------
/scripts/vis.py:
--------------------------------------------------------------------------------
1 | import glob
2 | import json
3 | import math
4 | import os
5 | import pickle
6 | import re
7 | from collections import defaultdict
8 |
9 | import cairosvg
10 | import matplotlib.pyplot as plt
11 | import numpy as np
12 | import svgwrite
13 | import tqdm.notebook as tq
14 | from IPython.display import HTML
15 | from matplotlib import animation
16 | from PIL import Image, ImageDraw
17 | from rdp import rdp
18 |
19 | from scripts.chars import map_chars
20 |
21 |
22 | def get_bounds(data):
23 | minx, miny = 600, 600
24 | maxx, maxy = 0, 0
25 |
26 | for i, (x, y, z) in enumerate(data):
27 | if minx > x:
28 | minx = x
29 | if miny > y:
30 | miny = y
31 |
32 | if maxx < x:
33 | maxx = x
34 | if maxy < y:
35 | maxy = y
36 | return minx, maxx, miny, maxy
37 |
38 | def convert_3d(drawing, return_flag = False, threshold=10):
39 | out = []
40 | corrupted = False
41 | for item in drawing:
42 | char = list(item.keys())[0]
43 | stroke = item[char]
44 | if len(stroke) == 1:
45 | x, y = stroke[0]
46 | out.append([x, y, 0])
47 | out.append([x+1, y+1, 1])
48 | continue
49 | segment = []
50 | for i, point in enumerate(stroke):
51 | x, y = point
52 | if i == len(stroke) - 1:
53 | segment.append([x, y, 1])
54 | else:
55 | segment.append([x, y, 0])
56 |
57 | start = 0
58 | for i, point in enumerate(segment):
59 | if i < len(segment) -1:
60 | x, y, _ = point
61 | next_x, next_y, _ = segment[i+1]
62 | if any((
63 | abs(x-next_x)>threshold,
64 | abs(y-next_y>threshold),
65 | )):
66 | corrupted=True
67 | start = i +1
68 | start = 0
69 | out += segment[start:]
70 | if return_flag:
71 | return out, corrupted
72 | return out
73 |
74 | def make_square(im, min_size=256, fill_color=(255, 255, 255)):
75 | x, y = im.size
76 | size = max(min_size, x, y)
77 | new_im = Image.new('RGBA', (size, size), fill_color)
78 | new_im.paste(im, (int((size - x) / 2), int((size - y) / 2)))
79 | return new_im
80 |
81 | def draw_strokes(data, factor=1, svg_filename = 'tmp/sample.svg', stroke_width = 3,
82 | square = False, return_res = False, crop = True):
83 |
84 | os.makedirs('tmp', exist_ok=True)
85 | if crop:
86 | min_x, max_x, min_y, max_y = get_bounds(data)
87 | else:
88 | min_x, max_x, min_y, max_y = 0, 600, 0, 600
89 |
90 | dims = (50 + max_x - min_x, 50 + max_y - min_y)
91 | dwg = svgwrite.Drawing(svg_filename, size = dims)
92 | dwg.add(dwg.rect(insert=(0, 0), size=dims,fill='white'))
93 | lift_pen = 1
94 | abs_x = 25 - min_x
95 | abs_y = 25 - min_y
96 | p = "M%s,%s " % (abs_x, abs_y)
97 | command = "M"
98 | for i in range(len(data)):
99 | if (lift_pen == 1):
100 | command = "M"
101 | elif (command != "L"):
102 | command = "L"
103 | else:
104 | command = ""
105 | x = float(data[i][0]) - min_x
106 | y = float(data[i][1]) - min_y
107 | lift_pen = data[i][2]
108 | p += command+str(x)+" "+str(y)+" "
109 | the_color = "black"
110 |
111 | dwg.add(dwg.path(p).stroke(the_color,stroke_width).fill("none"))
112 | dwg.save()
113 | cairosvg.svg2png(url="tmp/sample.svg", write_to="tmp/sample.png")
114 | img = Image.open('tmp/sample.png')
115 | if square:
116 | img = make_square(img)
117 | if return_res:
118 | return img, dims
119 | else:
120 | return img
121 |
122 | def apply_rdb(drawing, verbose = 0):
123 | new_drawing = []
124 | total_prev_strokes = 0
125 | total_post_strokes = 0
126 | for item in drawing:
127 | char = list(item.keys())[0]
128 | stroke = item[char]
129 | processed_stroke = []
130 | if len(stroke):
131 | if verbose:
132 | print('processing ', char)
133 | post_stroke = rdp(stroke, epsilon = 2.0)
134 | total_post_strokes += len(post_stroke)
135 | total_prev_strokes += len(stroke)
136 | new_drawing.append({char:post_stroke})
137 | if verbose:
138 | print('reduced from ', total_prev_strokes, ' to ', total_post_strokes)
139 | return new_drawing
140 |
141 | def preprocess(text):
142 | char_comps = []
143 |
144 | diacritics = "[ًٌٍَُِّْ]"
145 | numbers = '0123456789'
146 | for diac in diacritics:
147 | text = text.replace(diac, '')
148 |
149 | for num in numbers:
150 | text = text.replace(num, '')
151 |
152 | outText = ""
153 |
154 | for i in range(len(text)):
155 |
156 | if (text[i] == " "):
157 | continue
158 |
159 | if text[i] in map_chars:
160 | if (i < len(text) - 1 and text[i] == "\u0643"):
161 | if text[i+1] != ' ':
162 | char_comps.append({text[i] : '\uFEDB'})
163 | else:
164 | char_comps.append({text[i] : map_chars[text[i]]})
165 | else:
166 | char_comps.append({text[i] : map_chars[text[i]]})
167 | else:
168 | char_comps.append({text[i] : text[i]})
169 |
170 | return char_comps
171 |
172 | def concatenate(images, mode='h', margin=10):
173 | widths, heights = zip(*(i.size for i in images))
174 | if mode =='h':
175 | total_width = sum(widths)
176 | max_height = max(heights)
177 |
178 | new_im = Image.new('RGB', (total_width, max_height), (255, 255, 255))
179 |
180 | x_offset = 0
181 | for im in images[::-1]:
182 | new_im.paste(im, (x_offset,0))
183 | x_offset += im.size[0]
184 | elif mode == 'v':
185 | total_height = sum(heights)
186 | max_width = max(widths)
187 |
188 | new_im = Image.new('RGB', (max_width, total_height+margin*(len(images)-1)), (255, 255, 255))
189 | draw = ImageDraw.Draw(new_im)
190 | y_offset = 0
191 | for im in images:
192 | new_im.paste(im, (0,y_offset+margin))
193 | y_offset += im.size[1]
194 | draw.line((0,y_offset+margin-5, max_width,y_offset+margin-5), fill=(0, 0, 0), width=3)
195 | return new_im
196 |
197 | def generate_characters(file):
198 | char_drawings = []
199 | annot = file.split('/')[-1][:-5]
200 | char_comps = preprocess(annot)
201 | drawing = json.load(open(file))
202 | new_drawing = apply_rdb(drawing, verbose = 0)
203 | i = 0
204 | for comp in char_comps:
205 | char = list(comp.keys())[0]
206 | j = i + len(comp[char])
207 | char_drawings.append({char:new_drawing[i:j]})
208 | i = j
209 | return char_drawings
210 |
211 | def generate_words(file):
212 | word_drawings = []
213 | annot = file.split('/')[-1][:-5]
214 | annot = re.sub('[0-9]', '', annot)
215 | char_comps = preprocess(annot)
216 | indices = [m.start() - i - 1 for i, m in enumerate(re.finditer(' ', annot))]
217 | indices = indices + [len(annot) - len(indices)- 1]
218 | drawing = json.load(open(file))
219 | # new_drawing = apply_rdb(drawing, verbose = 0)
220 | i, j = 0, 0
221 | c = 0
222 | word = ""
223 | for cntr, comp in enumerate(char_comps):
224 | char = list(comp.keys())[0]
225 | j += len(comp[char])
226 | word += char
227 | if cntr == indices[c]:
228 | word_drawings.append({word:drawing[i:i+j]})
229 | i = i+j
230 | j = 0
231 | c += 1
232 | word = ""
233 | return word_drawings
234 |
235 | def get_annotation(json_path):
236 | return json_path.split('_')[0].split('/')[-1]
237 |
238 | def clean_text(text):
239 | char_comps = []
240 |
241 | diacritics = "[ًٌٍَُِّْ]"
242 |
243 | text = re.sub("[ًٌٍَُِّْ]", "", text)
244 | text = re.sub('[0-9]', '', text)
245 | text = re.sub('[a-zA-Zö\xa0]', '', text)
246 | return text.replace('_', '')
247 |
248 | def concatenate(images, mode='h', margin=10):
249 | widths, heights = zip(*(i.size for i in images))
250 | if mode =='h':
251 | total_width = sum(widths)
252 | max_height = max(heights)
253 |
254 | new_im = Image.new('RGB', (total_width, max_height), (255, 255, 255))
255 |
256 | x_offset = 0
257 | for im in images[::-1]:
258 | new_im.paste(im, (x_offset,0))
259 | x_offset += im.size[0]
260 | elif mode == 'v':
261 | total_height = sum(heights)
262 | max_width = max(widths)
263 |
264 | new_im = Image.new('RGB', (max_width, total_height+margin*(len(images)-1)), (255, 255, 255))
265 | draw = ImageDraw.Draw(new_im)
266 | y_offset = 0
267 | for im in images:
268 | new_im.paste(im, (0,y_offset+margin))
269 | y_offset += im.size[1]
270 | draw.line((0,y_offset+margin-5, max_width,y_offset+margin-5), fill=(0, 0, 0), width=3)
271 | return new_im
272 |
273 | def save_video(drawing, mx):
274 | num_sketches = 1
275 | global line_data, line
276 | line_data = ([], [])
277 | sqrt = int(math.sqrt(num_sketches))
278 | fig, ax = plt.subplots(sqrt, sqrt, figsize=(5,5))
279 | ax.set_ylim(600, 0)
280 | ax.set_xlim(0, 600)
281 | ax.set_xticks([])
282 | ax.set_yticks([])
283 | ax.set_aspect('equal', adjustable='box')
284 | line, = ax.plot([], [], lw=2, color = 'k')
285 |
286 | def data_gen():
287 | for point in drawing:
288 | yield point
289 |
290 |
291 | # initialize the data arrays
292 | def run(point):
293 | global line_data, line
294 | x, y, z = point
295 | line_data[0].append(x)
296 | line_data[1].append(y)
297 | line.set_data(line_data[0], line_data[1])
298 | if z == 1:
299 | line, = ax.plot([], [], lw=2, color = 'k')
300 | line_data = ([], [])
301 |
302 | return line
303 |
304 | ani = animation.FuncAnimation(fig, run, data_gen, interval=10, repeat=False, save_count=mx)
305 | d = ani.save(f'tmp/video.mp4', extra_args=['-vcodec', 'libx264'])
306 |
307 | def create_animation(json_path):
308 | max_count = 1000
309 | min_count = 100
310 |
311 | drawing = convert_3d(json.load(open(json_path)))
312 | save_video(drawing, len(drawing))
313 | return 'tmp/video.mp4'
314 |
--------------------------------------------------------------------------------