تمرين من المصدر الأصلي · تطبيق عملي

هذا الإصدار من التصفية

استخدم الشرح العربي للفهم، واعتمد صفحة CS50 الأصلية مرجعًا نهائيًا للمواصفات والتسليم.

جلسة دراسة مقترحة: حتى 60 دقيقةالجهد الكلي التقريبي: 120 دقيقة · أكمل من حيث توقفت لاحقًا.Week 4
محاور هذه الخطوة
  • المؤشرات
  • العناوين
  • malloc
  • الملفات

ترجمة آلية تحت المراجعة

هذا الإصدار من التصفية

المصدر الأصلي

قائمة تنفيذ التمرين

تُحفظ علامات هذه القائمة على هذا الجهاز والمتصفح فقط. أنشئ حسابًا مفعّلًا لحفظ إنجاز التمرين في مسارك.

التصفية

Harvard Yard with edge detection

مشكلة بحاجة إلى حل

ربما تكون أبسط طريقة لتمثيل الصورة هي باستخدام شبكة من وحدات البكسل (i.e.، نقاط)، يمكن أن يكون كل منها بلون مختلف. بالنسبة للصور بالأبيض والأسود، نحتاج إلى 1 بت لكل بكسل، حيث يمكن أن يمثل 0 اللون الأسود و1 يمكن أن يمثل اللون الأبيض، كما هو موضح أدناه.

a simple bitmap

وبهذا المعنى، فإن الصورة هي مجرد صورة نقطية (i.e.، خريطة من البتات). للحصول على المزيد من الصور الملونة، تحتاج ببساطة إلى عدد أكبر من البتات لكل بكسل. تنسيق الملف (مثل بي إم بي, JPEG، أو PNG) الذي يدعم "لون 24 بت" يستخدم 24 بت لكل بكسل. (يدعم BMP في الواقع الألوان 1 و4 و8 و16 و24 و32 بت.)

يستخدم BMP ذو 24 بت 8 بتات للإشارة إلى مقدار اللون الأحمر في لون البكسل، و8 بتات للإشارة إلى مقدار اللون الأخضر في لون البكسل، و8 بتات للإشارة إلى مقدار اللون الأزرق في لون البكسل. إذا كنت قد سمعت من قبل عن ألوان RGB، حسنًا، لقد حصلت عليها: الأحمر، الأخضر، الأزرق.

إذا كانت قيم R وG وB لبعض وحدات البكسل في BMP هي، على سبيل المثال، 0xff, 0x00و 0x00 بالنظام الست عشري، يكون هذا البكسل باللون الأحمر النقي، مثل 0xff (المعروف أيضًا باسم 255 بالنظام العشري) تشير إلى "الكثير من اللون الأحمر"، بينما 0x00 و 0x00 تشير ضمنًا إلى "لا يوجد اللون الأخضر" و"لا يوجد اللون الأزرق" على التوالي. في هذه المشكلة، ستتعامل مع قيم R وG وB لوحدات البكسل الفردية، مما يؤدي في النهاية إلى إنشاء مرشحات الصور الخاصة بك.

في ملف يسمى helpers.c في مجلد يسمى filter-more، اكتب برنامجًا لتطبيق المرشحات على BMPs.

تجريبي

رمز التوزيع

بالنسبة لهذه المشكلة، ستقوم بتوسيع وظيفة الشيفرة المقدمة لك من قبل موظفي CS50.

قم بتنزيل كود التوزيع

قم بتسجيل الدخول cs50.dev، انقر على نافذتك الطرفية، وقم بالتنفيذ cd بمفرده. يجب أن تجد أن مطالبة النافذة الطرفية الخاصة بك تشبه ما يلي:

$

التنفيذ التالي

wget https://cdn.cs50.net/2026/x/psets/4/filter-more.zip

من أجل تنزيل ملف ZIP يسمى filter-more.zip في مساحة الشيفرة الخاصة بك.

ثم نفذ

unzip filter-more.zip

لإنشاء مجلد يسمى filter-more. لم تعد بحاجة إلى ملف ZIP، حتى تتمكن من التنفيذ

rm filter-more.zip

ثم قم بالرد بـ "y" متبوعًا بـ Enter عند المطالبة لإزالة ملف ZIP الذي قمت بتنزيله.

الآن اكتب

cd filter-more

متبوعًا بـ Enter للانتقال إلى (i.e.، open) هذا الدليل. يجب أن تشبه مطالبتك الآن ما يلي.

filter-more/ $

تنفيذ ls بمفرده، ويجب أن تشاهد بعض الملفات: bmp.h, filter.c, helpers.h, helpers.cو Makefile. يجب أن تشاهد أيضًا مجلدًا يسمى images بأربعة ملفات BMP. إذا واجهت أي مشكلة، فاتبع نفس الخطوات مرة أخرى لترى ما إذا كان بإمكانك تحديد الخطأ الذي ارتكبته!

الخلفية

قليلا(خريطة) أكثر تقنية

تذكر أن الملف هو مجرد سلسلة من البتات، مرتبة بطريقة ما. إذن، فإن ملف BMP ذو 24 بت هو في الأساس مجرد سلسلة من البتات، (تقريبًا) كل 24 منها تمثل لونًا معينًا للبكسل. لكن ملف BMP يحتوي أيضًا على بعض "البيانات الوصفية"، وهي معلومات مثل ارتفاع الصورة وعرضها. يتم تخزين هذه البيانات الوصفية في بداية الملف في شكل بنيتين للبيانات يُشار إليهما عمومًا باسم "الرؤوس"، ويجب عدم الخلط بينها وبين ملفات رأس لغة C. (بالمناسبة، تطورت هذه الترويسات بمرور الوقت. وتستخدم هذه المشكلة أحدث إصدار من تنسيق BMP من Microsoft، 4.0، والذي ظهر لأول مرة مع نظام التشغيل Windows 95.)

تم استدعاء أول هذه الرؤوس BITMAPFILEHEADER، يبلغ طوله 14 بايت. (تذكر أن البايت الواحد يساوي 8 بتات.) يسمى الثاني من هذه الرؤوس BITMAPINFOHEADER، يبلغ طوله 40 بايت. مباشرة بعد هذه الرؤوس توجد الصورة النقطية الفعلية: مجموعة من البايتات، ثلاثة أضعافها تمثل لون البكسل. ومع ذلك، يقوم BMP بتخزين هذه الثلاثيات بشكل عكسي (i.e.، مثل BGR)، مع 8 بتات للون الأزرق، تليها 8 بتات للأخضر، تليها 8 بتات للون الأحمر. (تقوم بعض برامج BMP أيضًا بتخزين الصورة النقطية بأكملها بشكل عكسي، مع وجود الصف العلوي للصورة في نهاية ملف BMP. ولكننا قمنا بتخزين BMPs لمجموعة المشكلات هذه كما هو موضح هنا، مع الصف العلوي لكل صورة نقطية في الأول والصف السفلي في النهاية.) بمعنى آخر، إذا قمنا بتحويل الوجه المبتسم ذو 1 بت أعلاه إلى وجه مبتسم 24 بت، مع استبدال اللون الأحمر بالأسود، فإن BMP ذو 24 بت سيخزن هذه الصورة النقطية على النحو التالي، حيث 0000ff يعني اللون الأحمر و ffffff يعني اللون الأبيض؛ لقد أبرزنا باللون الأحمر جميع حالات 0000ff.

red smile

نظرًا لأننا قدمنا هذه الأجزاء من اليسار إلى اليمين، ومن الأعلى إلى الأسفل، في 8 أعمدة، يمكنك في الواقع رؤية الوجه المبتسم الأحمر إذا تراجعت خطوة إلى الوراء.

للتوضيح، تذكر أن الرقم السداسي العشري يمثل 4 بتات. وبناء على ذلك، ffffff بالنظام الست عشري يشير في الواقع 111111111111111111111111 في الثنائي.

لاحظ أنه يمكنك تمثيل الصورة النقطية كمصفوفة ثنائية الأبعاد من وحدات البكسل: حيث تكون الصورة عبارة عن مصفوفة من الصفوف، وكل صف عبارة عن مصفوفة من وحدات البكسل. في الواقع، هذه هي الطريقة التي اخترنا بها تمثيل الصور النقطية في هذه المشكلة.

تصفية الصور

ماذا يعني تصفية الصورة؟ يمكنك التفكير في تصفية صورة ما على أنها أخذ وحدات البكسل الخاصة ببعض الصور الأصلية، وتعديل كل بكسل بحيث يظهر تأثير معين في الصورة الناتجة.

تدرج الرمادي

أحد المرشحات الشائعة هو مرشح "التدرج الرمادي"، حيث نلتقط صورة ونريد تحويلها إلى أبيض وأسود. كيف يعمل هذا؟

تذكر أنه إذا تم ضبط القيم الحمراء والخضراء والزرقاء على 0x00 (النظام الست عشري لـ 0)، فإن البكسل أسود. وإذا تم ضبط كافة القيم على 0xff (النظام الست عشري لـ 255)، فيكون البكسل أبيض اللون. طالما أن قيم الأحمر والأخضر والأزرق كلها متساوية، فستكون النتيجة ظلالًا متفاوتة من اللون الرمادي على طول الطيف الأسود والأبيض، حيث تعني القيم الأعلى ظلالًا أفتح (أقرب إلى الأبيض) والقيم الأدنى تعني ظلالًا أغمق (أقرب إلى الأسود).

لذلك لتحويل بكسل إلى تدرج رمادي، نحتاج فقط إلى التأكد من أن قيم الأحمر والأخضر والأزرق كلها نفس القيمة. لكن كيف نعرف القيمة التي نصنعها؟ حسنًا، ربما يكون من المعقول أن نتوقع أنه إذا كانت القيم الأصلية للأحمر والأخضر والأزرق مرتفعة جدًا، فيجب أن تكون القيمة الجديدة مرتفعة أيضًا. وإذا كانت القيم الأصلية كلها منخفضة، فيجب أن تكون القيمة الجديدة منخفضة أيضًا.

في الواقع، للتأكد من أن كل بكسل في الصورة الجديدة لا يزال يتمتع بنفس السطوع أو الظلام العام مثل الصورة القديمة، يمكننا أن نأخذ متوسط ​​قيم الأحمر والأخضر والأزرق لتحديد ظل اللون الرمادي الذي يصنع البكسل الجديد.

إذا قمت بتطبيق ذلك على كل بكسل في الصورة، فستكون النتيجة صورة محولة إلى تدرج رمادي.

تأمل

قد تقوم بعض المرشحات أيضًا بتحريك وحدات البكسل. إن عكس الصورة، على سبيل المثال، هو عبارة عن مرشح حيث تكون الصورة الناتجة هي ما ستحصل عليه عن طريق وضع الصورة الأصلية أمام المرآة. لذا فإن أي بكسل على الجانب الأيسر من الصورة يجب أن ينتهي على الجانب الأيمن، والعكس صحيح.

لاحظ أن جميع وحدات البكسل الأصلية للصورة الأصلية ستظل موجودة في الصورة المنعكسة، لكن ربما تم إعادة ترتيب تلك البكسلات لتكون في مكان مختلف في الصورة.

طمس

هناك عدد من الطرق لإنشاء تأثير تمويه الصورة أو تنعيمها. بالنسبة لهذه المشكلة، سنستخدم "التعتيم المربع"، الذي يعمل عن طريق أخذ كل بكسل، وإعطائه قيمة جديدة لكل قيمة لون عن طريق حساب متوسط ​​قيم الألوان لوحدات البكسل المجاورة.

خذ بعين الاعتبار شبكة البكسلات التالية، حيث قمنا بترقيم كل بكسل.

a grid of pixels

ستكون القيمة الجديدة لكل بكسل هي متوسط ​​قيم جميع وحدات البكسل الموجودة ضمن صف وعمود واحد من البكسل الأصلي (مشكلاً مربعًا 3x3). على سبيل المثال، سيتم الحصول على كل قيمة من قيم الألوان للبكسل 6 عن طريق حساب متوسط ​​قيم الألوان الأصلية للبكسلات 1 و2 و3 و5 و6 و7 و9 و10 و11 (لاحظ أن البكسل 6 نفسه مضمن في المتوسط). وبالمثل، سيتم الحصول على قيم الألوان للبكسل 11 عن طريق حساب متوسط قيم الألوان للبكسلات 6 و7 و8 و10 و11 و12 و14 و15 و16.

بالنسبة إلى البكسل الموجود على طول الحافة أو الزاوية، مثل البكسل 15، سنظل نبحث عن جميع البكسلات الموجودة في صف وعمود واحد: في هذه الحالة، البكسلات 10 و11 و12 و14 و15 و16.

الحواف

في خوارزميات الذكاء الاصطناعي لمعالجة الصور، غالبًا ما يكون من المفيد اكتشاف الحواف في الصورة: الخطوط الموجودة في الصورة التي تنشئ حدودًا بين كائن وآخر. إحدى الطرق لتحقيق هذا التأثير هي تطبيق مشغل سوبل للصورة.

مثل تمويه الصورة، يعمل اكتشاف الحواف أيضًا عن طريق التقاط كل بكسل وتعديله بناءً على شبكة 3x3 من وحدات البكسل التي تحيط بهذا البكسل. ولكن بدلاً من مجرد أخذ متوسط ​​البكسلات التسعة، يقوم مشغل Sobel بحساب القيمة الجديدة لكل بكسل عن طريق أخذ مجموع مرجح لقيم البكسلات المحيطة. وبما أن الحواف بين الكائنات يمكن أن تحدث في الاتجاه الرأسي والأفقي، فسوف تقوم في الواقع بحساب مجموعين مرجحين: أحدهما لاكتشاف الحواف في الاتجاه x، والآخر لاكتشاف الحواف في الاتجاه y. على وجه الخصوص، ستستخدم "النواتين" التاليتين:

Sobel kernels

كيف نفسر هذه النوى؟ باختصار، لكل قيمة من قيم الألوان الثلاثة لكل بكسل، سنقوم بحساب قيمتين Gx و Gy. لحساب Gx بالنسبة لقيمة القناة الحمراء للبكسل، على سبيل المثال، سنأخذ القيم الحمراء الأصلية للبكسلات التسعة التي تشكل مربعًا 3x3 حول البكسل، ونضربها في القيمة المقابلة في Gx نواة، وخذ مجموع القيم الناتجة.

لماذا هذه القيم المحددة للنواة؟ في في اتجاه Gx ، على سبيل المثال، نقوم بضرب وحدات البكسل الموجودة على يمين البكسل المستهدف برقم موجب، وضرب وحدات البكسل الموجودة على يسار البكسل المستهدف برقم سالب. عندما نأخذ المجموع، إذا كانت وحدات البكسل الموجودة على اليمين مشابهة للون وحدات البكسل الموجودة على اليسار، فستكون النتيجة قريبة من 0 (يتم إلغاء الأرقام). ولكن إذا كانت وحدات البكسل الموجودة على اليمين مختلفة تمامًا عن وحدات البكسل الموجودة على اليسار، فستكون القيمة الناتجة إيجابية جدًا أو سلبية جدًا، مما يشير إلى تغير في اللون من المحتمل أن يكون نتيجة للحدود بين الكائنات. وتنطبق حجة مماثلة لحساب الحواف في y الاتجاه.

باستخدام هذه النوى، يمكننا إنشاء ملف قيمة Gx و Gy لكل قناة من القنوات الحمراء والخضراء والزرقاء للبيكسل. لكن كل قناة يمكن أن تأخذ قيمة واحدة فقط، وليس اثنتين: لذلك نحن بحاجة إلى طريقة ما للدمج Gx و Gy في قيمة واحدة. تجمع خوارزمية مرشح Sobel Gx و Gy إلى قيمة نهائية عن طريق حساب الجذر التربيعي لـ Gx^2 + Gy^2. وبما أن قيم القناة يمكن أن تأخذ فقط قيمًا صحيحة من 0 إلى 255، فتأكد من تقريب القيمة الناتجة إلى أقرب عدد صحيح وتحديد الحد الأقصى عند 255!

وماذا عن التعامل مع وحدات البكسل الموجودة على الحافة أو في زاوية الصورة؟ هناك العديد من الطرق للتعامل مع وحدات البكسل الموجودة على الحافة، ولكن لأغراض هذه المشكلة، سنطلب منك التعامل مع الصورة كما لو كان هناك حد أسود متصل بحجم 1 بكسل حول حافة الصورة: لذلك، يجب التعامل مع محاولة الوصول إلى بكسل يتجاوز حافة الصورة على أنها بكسل أسود خالص (قيم 0 لكل من الأحمر والأخضر والأزرق). سيؤدي هذا إلى تجاهل تلك البكسلات بشكل فعال من حساباتنا Gx و Gy.

المواصفات

تنفيذ الوظائف في helpers.c بحيث يمكن للمستخدم تطبيق مرشحات التدرج الرمادي أو الانعكاس أو التمويه أو اكتشاف الحواف على صوره. يجب أن تلتقط وظيفة

  • الوظيفة grayscale التقاط صورة وتحويلها إلى نسخة بالأبيض والأسود لنفس الصورة.
  • ال يتم استخدام الدالة reflect الصورة وتعكسها أفقيًا.
  • ال يتم استخدام الدالة blur بالتقاط صورة وتحويلها إلى نسخة مربعة غير واضحة من نفس الصورة.
  • ال يتم استخدام الدالة edges صورة وتسلط الضوء على الحواف بين الكائنات، وفقًا لمشغل Sobel.

لا يجوز لك تعديل أي من توقيعات الوظائف، كما لا يجوز لك تعديل أي ملفات أخرى بخلاف helpers.c.

الفهم

دعنا الآن نلقي نظرة على بعض الملفات المقدمة لك كرمز توزيع لفهم ما بداخلها.

bmp.h

افتح bmp.h (مثل النقر المزدوج عليه في متصفح الملفات) وإلقاء نظرة.

ستشاهد تعريفات الرؤوس التي ذكرناها (BITMAPINFOHEADER و BITMAPFILEHEADER). وبالإضافة إلى ذلك، يحدد هذا الملف BYTE, DWORD, LONGو WORD، أنواع البيانات الموجودة عادة في عالم برمجة Windows. لاحظ كيف أنها مجرد أسماء مستعارة للأوليات التي (نأمل) أن تكون على دراية بها بالفعل. يبدو أن BITMAPFILEHEADER و BITMAPINFOHEADER الاستفادة من هذه الأنواع.

ولعل الأهم بالنسبة لك هو أن هذا الملف يعرّف أيضًا ملف struct RGBTRIPLE الذي، بكل بساطة، "يحتوي" على ثلاث بايتات: واحدة زرقاء، وواحدة خضراء، وواحدة حمراء (الترتيب، كما نتذكر، الذي نتوقع فيه العثور على ثلاثيات RGB فعليًا على القرص).

لماذا هذه structمفيد؟ حسنًا، تذكر أن الملف هو مجرد سلسلة من البايتات (أو في النهاية البتات) الموجودة على القرص. ولكن يتم ترتيب هذه البايتات بشكل عام بحيث تمثل البايتات القليلة الأولى شيئًا ما، وتمثل البايتات القليلة التالية شيئًا آخر، وهكذا. توجد "تنسيقات الملفات" لأن العالم قام بتوحيد ما تعنيه وحدات البايت. الآن، يمكننا فقط قراءة ملف من القرص إلى ذاكرة الوصول العشوائي (RAM) كمصفوفة واحدة كبيرة من البايتات. ويمكننا أن نتذكر فقط أن البايت عند array[i] يمثل شيئًا واحدًا، بينما البايت عند array[j] يمثل آخر. ولكن لماذا لا نذكر أسماء بعض تلك البايتات حتى نتمكن من استرجاعها من الذاكرة بسهولة أكبر؟ هذا هو بالضبط ما الهياكل فيه bmp.h اسمح لنا بذلك. بدلاً من التفكير في بعض الملفات على أنها تسلسل طويل من البايتات، يمكننا بدلاً من ذلك التفكير فيها على أنها تسلسل من structق.

filter.c

الآن، دعونا ننفتح filter.c. لقد تم كتابة هذا الملف لك بالفعل، ولكن هناك بعض النقاط المهمة الجديرة بالملاحظة هنا.

أولاً، لاحظ تعريف filters في السطر 10. تخبر هذه السلسلة البرنامج ما هي وسائط سطر الأوامر المسموح بها للبرنامج: b, e, gو r. يحدد كل واحد منهم مرشحًا مختلفًا قد نطبقه على صورنا: التمويه، واكتشاف الحواف، والتدرج الرمادي، والانعكاس.

تفتح الأسطر العديدة التالية ملف صورة، وتتأكد من أنه ملف BMP بالفعل، وتقرأ جميع معلومات البكسل في مصفوفة ثنائية الأبعاد تسمى image.

قم بالتمرير لأسفل إلى عبارة switch التي تبدأ بالسطر 101. لاحظ ذلك، اعتمادًا على ماذا filter الذي اخترناه، يتم استدعاء وظيفة مختلفة: إذا اختار المستخدم عامل التصفية b، يستدعي البرنامج وظيفة blur ؛ إذا تم استدعاء eإذن يجب طباعة edges ؛ إذا تم استدعاء gإذن يجب طباعة grayscale ؛ وإذا تم استدعاء rإذن يجب طباعة reflect . لاحظ أيضًا أن كل واحدة من هذه الوظائف تأخذ ارتفاع الصورة وعرضها ومصفوفة البكسلات ثنائية الأبعاد كمتغيرات.

هذه هي الوظائف التي ستنفذها (قريبًا!). كما قد تتخيل، فإن الهدف هو أن تقوم كل وظيفة من هذه الوظائف بتحرير مصفوفة ثنائية الأبعاد من وحدات البكسل بطريقة يتم فيها تطبيق المرشح المطلوب على الصورة.

تأخذ الأسطر المتبقية من البرنامج الناتج image واكتبها في ملف صورة جديد.

helpers.h

بعد ذلك، ألقِ نظرة على helpers.h. هذا الملف قصير جدًا، ويوفر فقط النماذج الأولية للوظائف التي رأيتها سابقًا.

هنا، لاحظ حقيقة أن كل وظيفة تأخذ مصفوفة ثنائية الأبعاد تسمى image كوسيطة، حيث image عبارة عن مصفوفة من height العديد من الصفوف، وكل صف هو في حد ذاته مصفوفة أخرى width كثيرة RGBTRIPLEق. فإذا image يمثل الصورة بأكملها إذن image[0] يمثل الصف الأول، و يمثل image[0][0] البكسل الموجود في الزاوية العلوية اليسرى من الصورة.

helpers.c

الآن، افتح helpers.c. هنا يتم تنفيذ الوظائف المعلنة فيها helpers.h ينتمون. لكن لاحظ أن التطبيقات مفقودة الآن! هذا الجزء متروك لك.

Makefile

أخيرًا، دعونا ننظر Makefile. يحدد هذا الملف ما يجب أن يحدث عندما نقوم بتشغيل أمر طرفي مثل make filter. في حين أن البرامج التي ربما تكون قد كتبتها من قبل كانت مقتصرة على ملف واحد فقط، يبدو أن filter يستخدم ملفات متعددة: filter.c و helpers.c. لذلك علينا أن نقول make كيفية تجميع هذا الملف.

حاول التجميع filter بنفسك عن طريق الذهاب إلى جهازك الطرفي والتشغيل

$ make filter

ثم يمكنك تشغيل البرنامج عن طريق تشغيل:

$ ./filter -g images/yard.bmp out.bmp

الذي يأخذ الصورة في images/yard.bmp، ويقوم بإنشاء صورة جديدة تسمى out.bmp بعد تشغيل وحدات البكسل من خلال grayscale . grayscale لم يفعل أي شيء حتى الآن، لذا يجب أن تبدو الصورة الناتجة مثل الفناء الأصلي.

تلميحات

  • قيم البكسل مكونات rgbtRed, rgbtGreenو rgbtBlue كلها أعداد صحيحة، لذا تأكد من تقريب أي أرقام الفاصلة العائمة إلى أقرب عدد صحيح عند تعيينها إلى قيمة بكسل!

الإرشادات التفصيلية

يرجى ملاحظة أن هناك 5 مقاطع فيديو في قائمة التشغيل هذه. مفتوح على YouTube

محتوى خارجي من YouTube

لن نتصل بهذه الخدمة قبل موافقتك. يمكنك فتح المصدر في نافذة مستقلة بدل تحميله هنا.

فتح المصدر

كيفية الاختبار

تأكد من اختبار كافة المرشحات الخاصة بك على نماذج ملفات الصور النقطية المتوفرة!

صحة

check50 cs50/problems/2026/x/filter/more

النمط

style50 helpers.c

كيفية الإرسال

في جهازك الطرفي، قم بتنفيذ ما يلي لإرسال عملك، والإجابة على المطالبات التي تظهر أيضًا.

submit50 cs50/problems/2026/x/filter/more

أنهيت التطبيق؟

راجع عناصر التنفيذ أعلاه، ثم احفظ إنجازك وانتقل إلى التمرين التالي.

حفظ إنجاز التمرين

يمكنك قراءة الدورة كاملة دون حساب. يصبح حفظ التقدم متاحًا بعد تفعيل الحساب، ويبقى محفوظًا بعد انتهاء العضوية.

العودة إلى جميع التمارين