معالجة الصور باستخدام مكتبة Pillow

يتيح لك Pillow معالجة الصور وإجراء مهام معالجة الصور الأساسية. باعتباره فرعًا من مكتبة Python Imaging Library (PIL)، يدعم Pillow صيغ الصور مثل JPEG وPNG وغيرها، مما يتيح لك قراءة الصور وتحريرها وحفظها. باستخدام Pillow، يمكنك قص الصور وتغيير حجمها وتدويرها وتطبيق الفلاتر عليها، مما يجعله أداة متعددة الاستخدامات لمعالجة الصور.

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

عمليات الصور الأساسية باستخدام مكتبة Pillow في بايثون

مكتبة بايثون Pillow هي فرع من مكتبة قديمة تُسمى PIL. PIL اختصار لـ Python Imaging Library، وهي المكتبة الأصلية التي مكّنت بايثون من التعامل مع الصور. تم إيقاف PIL عام ٢٠١١، وهي تدعم بايثون ٢ فقط. وباستخدام وصف مطوريها، فإن Pillow هي فرع PIL سهل الاستخدام الذي حافظ على استمرارية المكتبة، ويتضمن دعمًا لبايثون ٣.

تحتوي بايثون على أكثر من وحدة للتعامل مع الصور ومعالجتها. إذا كنت ترغب في التعامل مع الصور مباشرةً من خلال تعديل بكسلاتها، يمكنك استخدام NumPy وSciPy. ومن المكتبات الشائعة الأخرى لمعالجة الصور: OpenCV وscikit-image وMahotas. بعض هذه المكتبات أسرع وأقوى من Pillow.

مع ذلك، يبقى Pillow أداةً مهمةً لمعالجة الصور. فهو يوفر ميزات معالجة صور مشابهة لتلك الموجودة في برامج معالجة الصور مثل Photoshop. ويُعدّ Pillow الخيار الأمثل غالبًا لمهام معالجة الصور عالية المستوى التي لا تتطلب خبرةً متقدمة. كما يُستخدم غالبًا في الأعمال الاستكشافية عند التعامل مع الصور.

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

ستحتاج إلى تثبيت المكتبة قبل استخدامها. يمكنك تثبيت Pillow باستخدام pip في بيئة افتراضية:

PS> python -m venv venv
PS> .venvScriptsactivate
(venv) PS> python -m pip install Pillow

الآن بعد أن قمت بتثبيت الحزمة، فأنت جاهز لبدء التعرف على مكتبة Pillow وإجراء المعالجات الأساسية للصور.

وحدة Image وفئة Image في Pillow

الفئة الرئيسية المُعرّفة في Pillow هي فئة Image. عند قراءة صورة باستخدام Pillow، تُخزّن الصورة في كائن من نوع Image.

عند استكشاف الصور باستخدام Pillow، يُفضّل استخدام بيئة REPL تفاعلية.

>>> from PIL import Image
>>> filename = "buildings.jpg"
>>> with Image.open(filename) as img:
...     img.load()
...

>>> type(img)
<class 'PIL.JpegImagePlugin.JpegImageFile'>

>>> isinstance(img, Image.Image)
True

قد تتوقع الاستيراد من Pillow بدلاً من PIL. في النهاية، قمت بتثبّيت Pillow، وليس PIL. مع ذلك، Pillow هو فرع من مكتبة PIL. لذلك، ستظل بحاجة إلى استخدام PIL عند الاستيراد إلى الكود.

يمكنك استدعاء الدالة ()open لقراءة الصورة من الملف، و.load() لقراءة الصورة في الذاكرة، مما يسمح بإغلاق الملف. يمكنك استخدام عبارة with لإنشاء مدير سياق لضمان إغلاق الملف فور انتهاء الحاجة إليه.

في هذا المثال، الكائن هو نوع خاص بصور JPEG، وهو فئة فرعية من فئة Image، كما تؤكد ذلك باستدعاء ()isinstance. لاحظ أن كلاً من الفئة والوحدة التي عُرفت بها الفئة تحملان الاسم نفسه، Image. يمكنك عرض الصورة باستخدام .show():

>>> img.show()

تحفظ دالة .show() الصورة كملف مؤقت وتعرضها باستخدام برنامج نظام التشغيل الخاص بك لمعالجة الصور. عند تشغيل الكود أعلاه، ستظهر الصورة التالية:

في بعض الأنظمة، سيؤدي استدعاء .show() إلى حظر REPL حتى إغلاق الصورة. يعتمد ذلك على نظام التشغيل وبرنامج عرض الصور الافتراضي الذي تستخدمه.

ستحتاج إلى الإلمام بثلاث خصائص رئيسية عند التعامل مع الصور في مكتبة بايثون Pillow. يمكنك استكشافها باستخدام سمات فئة الصورة .format و.size و.mode:

>>> img.format
'JPEG'

>>> img.size
(1920, 1273)

>>> img.mode
'RGB'

يُظهر تنسيق الصورة نوع الصورة التي تتعامل معها. في هذه الحالة، تنسيق الصورة هو JPEG. يُظهر الحجم عرض الصورة وارتفاعها بالبكسل. وضع الصورة هو RGB. ستتعرف على المزيد حول الأوضاع قريبًا.

في كثير من الأحيان، قد تحتاج إلى اقتصاص الصور وتغيير حجمها. تحتوي فئة Image على تابعين يمكنك استخدامهما لإجراء هذه العمليات: .crop() و.resize():

>>> cropped_img = img.crop((300, 150, 700, 1000))
>>> cropped_img.size
(400, 850)

>>> cropped_img.show()

>>> low_res_img = cropped_img.resize(
...     (cropped_img.width // 4, cropped_img.height // 4)
... )
>>> low_res_img.show()

يجب أن تكون وسيطة .crop() مكونة من أربعة عناصر تُحدد الحواف اليسرى والعلوية واليمنى والسفلية للمنطقة المراد اقتصاصها. يُعيّن نظام الإحداثيات المُستخدم في Pillow الإحداثيات (0، 0) للبكسل في الزاوية العلوية اليسرى. وهو نفس نظام الإحداثيات المُستخدم عادةً للمصفوفات ثنائية الأبعاد. يُمثل المكون من أربعة عناصر القسم التالي من الصورة:

الصورة الجديدة التي تُرجعها دالة .crop() في الكود أعلاه بحجم 400×850 بكسل. تُظهر الصورة المقصوصة مبنىً واحدًا فقط من الصورة الأصلية.

في الكود أعلاه، يمكنك أيضًا تغيير دقة الصورة المقصوصة باستخدام دالة .resize()، والتي تتطلب قيمةً مُحددةً كمُعاملٍ إلزامي. تُحدد القيمة المُحددة عرض الصورة وارتفاعها الجديدين بالبكسل.

في المثال أعلاه، يتم ضبط العرض والارتفاع الجديدين إلى ربع قيمتيهما الأصلية باستخدام عامل قسمة الأرضية (//) وسمتي  الصورة .width و.height. يعرض الاستدعاء الأخير لـ ()show الصورة المقصوصة والمُعاد حجمها:

هناك معلمات اختيارية إضافية يمكنك استخدامها مع .resize() للتحكم في كيفية إعادة أخذ عينات من الصورة. أو يمكنك تحقيق تغيير مماثل في الحجم باستخدام .reduce():

>>> low_res_img = cropped_img.reduce(4)

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

بمجرد رضاك ​​عن الصورة التي تم إرجاعها، يمكنك حفظ أي من كائنات Image في ملف باستخدام .save():

>>> cropped_img.save("cropped_image.jpg")
>>> low_res_img.save("low_resolution_cropped_image.png")

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

معالجة الصور الأساسية

يمكنك تعديل الصورة بما يتجاوز القص وتغيير الحجم. ومن المتطلبات الشائعة الأخرى تدوير الصورة أو قلبها. يمكنك استخدام دالة .transpose() لبعض التحويلات. تابع بنفس جلسة REPL التي بدأتها في القسم السابق:

>>> converted_img = img.transpose(Image.FLIP_TOP_BOTTOM)
>>> converted_img.show()

يعرض هذا الكود الصورة التالية:

هناك سبعة خيارات يمكنك تمريرها كوسائط إلى .transpose():

  • Image.FLIP_LEFT_RIGHT: تقلب الصورة من اليسار إلى اليمين، مما ينتج عنه صورة معكوسة
  • Image.FLIP_TOP_BOTTOM: تقلب الصورة من الأعلى إلى الأسفل
  • Image.ROTATE_90: تدوير الصورة بمقدار 90 درجة عكس اتجاه عقارب الساعة
  • Image.ROTATE_180: تدوير الصورة بمقدار 180 درجة
  • Image.ROTATE_270: تدوير الصورة بمقدار 270 درجة عكس اتجاه عقارب الساعة، وهو نفس تدوير الصورة بمقدار 90 درجة في اتجاه عقارب الساعة
  • Image.TRANSPOSE: ينقل الصفوف والأعمدة باستخدام بكسل أعلى اليسار كأصل، مع كون بكسل أعلى اليسار هو نفسه في الصورة المنقولة كما هو الحال في الصورة الأصلية
  • Image.TRANSVERSE: ينقل الصفوف والأعمدة باستخدام بكسل أسفل اليسار كأصل، مع كون بكسل أسفل اليسار هو الذي يبقى ثابتًا بين الإصدارين الأصلي والمعدل

جميع خيارات التدوير المذكورة أعلاه تُعرّف التدوير بخطوات 90 درجة. إذا كنت ترغب في تدوير صورة بزاوية أخرى، يمكنك استخدام الدالة .rotate():

>>> rotated_img = img.rotate(45)
>>> rotated_img.show()

تؤدي هذه الطريقة إلى تدوير الصورة بمقدار 45 درجة عكس اتجاه عقارب الساعة، مما يعطي الصورة التالية:

حجم كائن Image المُعاد هو نفس حجم الصورة الأصلية. لذلك، زوايا الصورة مفقودة في هذا العرض. يمكنك تغيير هذا السلوك باستخدام مُعامل  expand المُسمّى:

>>> rotated_img = img.rotate(45, expand=True)
>>> rotated_img.show()

تعيد هذه الطريقة صورة أكبر تحتوي بالكامل على الصورة التي تم تدويرها:

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

نطاقات وأوضاع الصورة في مكتبة Pillow

الصورة عبارة عن مصفوفة ثنائية الأبعاد من وحدات البكسل، حيث يُمثل كل بكسل لونًا. يمكن تمثيل كل بكسل بقيمة واحدة أو أكثر. على سبيل المثال، في صورة RGB، يُمثل كل بكسل بثلاث قيم تُمثل قيم الأحمر والأخضر والأزرق لذلك البكسل.

لذلك، يحتوي كائن Image لصورة RGB على ثلاثة نطاقات، نطاق لكل لون. تُمثَّل صورة RGB بحجم 100×100 بكسل بمصفوفة قيم 100×100×3.

تتضمن صور RGBA أيضًا قيمة ألفا، التي تحتوي على معلومات حول شفافية كل بكسل. تحتوي صورة RGBA على أربعة نطاقات، نطاق لكل لون، ونطاق رابع يحتوي على قيم ألفا. لكل نطاق أبعاد مطابقة لأبعاد الصورة. لذلك، تُمثَّل صورة RGBA بحجم 100×100 بكسل بمصفوفة قيم 100×100×4.

يصف وضع الصورة نوع الصورة التي تعمل عليها. يدعم Pillow معظم الأوضاع القياسية، بما في ذلك الأبيض والأسود (ثنائي)، وتدرج الرمادي، وRGB، وRGBA، وCMYK. يمكنك الاطلاع على القائمة الكاملة للأوضاع المدعومة في وثائق Pillow.

يمكنك معرفة عدد الأشرطة في كائن صورة باستخدام دالة .getbands()، ويمكنك التحويل بين الأوضاع باستخدام .convert(). ستستخدم الآن الصورة التالية لهذا الدرس.

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

>>> filename = "strawberry.jpg"
>>> with Image.open(filename) as img:
...     img.load()
...

>>> cmyk_img = img.convert("CMYK")
>>> gray_img = img.convert("L")  # Grayscale

>>> cmyk_img.show()
>>> gray_img.show()

>>> img.getbands()
('R', 'G', 'B')
>>> cmyk_img.getbands()
('C', 'M', 'Y', 'K')
>>> gray_img.getbands()
('L',)

يمكنك استدعاء دالة .convert() مرتين لتحويل صورة RGB إلى صورة CMYK وصورة تدرج الرمادي. تبدو صورة CMYK مشابهة للصورة الأصلية، ولكنها مُرمَّزة باستخدام الوضع الشائع في المواد المطبوعة بدلاً من الشاشات الرقمية. يُعطي التحويل إلى تدرج الرمادي الناتج التالي:

تؤكد المخرجات من المكالمات إلى .getbands() وجود ثلاثة نطاقات في صورة RGB، وأربعة نطاقات في صورة CMYK، ونطاق واحد في الصورة ذات التدرج الرمادي.

يمكنك فصل صورة إلى نطاقاتها باستخدام دالة .split()، ثم دمج النطاقات المنفصلة في كائن Image باستخدام دالة merge(). عند استخدام دالة .split()، تُرجع الدالة جميع النطاقات ككائنات صورة منفصلة. يمكنك التأكد من ذلك بعرض التمثيل النصي لأحد الكائنات المُعادة:

>>> red, green, blue = img.split()
>>> red
<PIL.Image.Image image mode=L size=1920x1281 at 0x7FDD80C9AFA0>
>>> red.mode
'L'

وضع الكائن الذي يعيده .split() هو ‘L’، مما يشير إلى أن هذه صورة بدرجات الرمادي، أو صورة تعرض فقط قيم سطوع كل بكسل.

الآن، يمكنك إنشاء ثلاث صور RGB جديدة تعرض القنوات الحمراء والخضراء والزرقاء بشكل منفصل باستخدام ()merge، وهي دالة في وحدة Image:

>>> zeroed_band = red.point(lambda _: 0)

>>> red_merge = Image.merge(
...     "RGB", (red, zeroed_band, zeroed_band)
... )

>>> green_merge = Image.merge(
...     "RGB", (zeroed_band, green, zeroed_band)
... )

>>> blue_merge = Image.merge(
...     "RGB", (zeroed_band, zeroed_band, blue)
... )

>>> red_merge.show()
>>> green_merge.show()
>>> blue_merge.show()

تحدد الوسيطة الأولى في دالة ()merge وضع الصورة المراد إنشاؤها. أما الوسيطة الثانية فتتضمن الأشرطة الفردية التي تريد دمجها في صورة واحدة.

الشريط الأحمر وحده، المُخزَّن في المتغير “red“، هو صورة بدرجات الرمادي مع الوضع “L”. لإنشاء صورة تُظهر القناة الحمراء فقط، يُدمج الشريط الأحمر من الصورة الأصلية مع الشريطين الأخضر والأزرق اللذين يحتويان على أصفار فقط. لإنشاء شريط يحتوي على أصفار في كل مكان، يُستخدم الدالة .point()

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

عند دمج الشريط الأحمر مع الشريطين الأخضر والأزرق اللذين يحتويان على أصفار، ستحصل على صورة RGB تُسمى red_merge. لذلك، تحتوي صورة RGB التي تُنشئها فقط على قيم غير صفرية في القناة الحمراء، ولكن لأنها لا تزال صورة RGB، فسيتم عرضها بالألوان.

كرر عملية مماثلة للحصول على دمج أخضر ودمج أزرق، اللذين يحتويان على صور RGB مع قنوات خضراء وزرقاء من الصورة الأصلية. يعرض الكود الصور الثلاث التالية:

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

معالجة الصور باستخدام Pillow في بايثون

لقد تعلمتَ كيفية قصّ الصور وتدويرها، وتغيير حجمها، واستخراج أشرطة الألوان منها. مع ذلك، لم تُحدث أيٌّ من الإجراءات التي اتخذتها حتى الآن أي تغيير في محتوى الصورة. في هذا القسم، ستتعلم ميزات معالجة الصور في مكتبة Pillow بلغة بايثون. ستستخدم وحدة ImageFilter في Pillow.

مرشحات الصور باستخدام نوى الالتفاف

إحدى الطرق المستخدمة في معالجة الصور هي التفاف الصور باستخدام النوى. لا يهدف هذا البرنامج التعليمي إلى تقديم شرح مفصل لنظرية معالجة الصور.

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

يمكنك استخدام صورة بسيطة لفهم عملية الالتفاف باستخدام النوى. حجم الصورة 30×30 بكسل، وتحتوي على خط عمودي ونقطة. عرض الخط أربعة بكسلات، والنقطة مربعة 4×4 بكسلات. الصورة أدناه مُكبّرة لأغراض العرض.

يمكنك وضع النواة في أي مكان في الصورة واستخدام موقع الخلية المركزية للنواة كمرجع. الرسم البياني أدناه يُمثل الجزء العلوي الأيسر من الصورة.

تمثل العناصر الموجودة في هذا الرسم التخطيطي جوانب مختلفة من الصورة والنواة:

  • تمثل المربعات البيضاء وحدات البكسل في الصورة التي لها قيمة 0.
  • تمثل المربعات الحمراء وحدات البكسل في الصورة التي لها قيمة 255. وهي تشكل النقطة في الصورة المعروضة أعلاه.
  • كل منطقة أرجوانية تُمثل النواة. تتكون هذه النواة من منطقة ٣×٣، ولكل خلية فيها قيمة ١/٩. يُظهر الرسم التخطيطي النواة في ثلاثة مواضع مختلفة مُسمّاة بالأرقام ١، ٢، و٣.

يمكن إنشاء صورة جديدة نتيجةً لالتواء الصورة مع النواة. يمكنك فهم عملية الالتفاف من خلال الخطوات التالية:

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

يمكنك رؤية هذه العملية مع مواضع النواة الثلاثة المُسمّاة بالأرقام 1 و2 و3 في الرسم التخطيطي أعلاه. لنأخذ موضع النواة المُسمّى بالرقم 1. موضع هذه النواة هو (3، 2)، وهو موضع خليتها المركزية لأنها تقع في الصف الرابع (الفهرس = 3) والعمود الثالث (الفهرس = 2). قيمة كل بكسل صورة في المنطقة التي تغطيها النواة تساوي صفرًا.

وبالتالي، ستكون جميع عمليات الضرب من الخطوة ٢ صفرًا، ومجموعها صفرًا أيضًا. ستكون قيمة الصورة الجديدة صفرًا عند البكسل (٣، ٢).

يختلف السيناريو بالنسبة لمواضع النواة الأخرى الموضحة. لنفترض أن النواة المسمى 2 تقع عند (4، 7). إحدى بكسلات الصورة المتداخلة معها ليست صفرًا. بضرب قيمة هذه البكسل بقيمة النواة، نحصل على 255 × (1/9) = 28.33. تبقى عمليات الضرب الثمانية المتبقية صفرًا لأن بكسلات الصورة صفر. وبالتالي، ستكون قيمة البكسل في الموضع (4، 7) في الصورة الجديدة 28.33.

موضع النواة الثالث الموضح أعلاه يقع عند (8، 11). توجد أربع بكسلات صورة غير صفرية متداخلة مع هذه النواة. قيمة كل بكسل منها 255، لذا ستكون نتيجة الضرب 28.33 لكل موضع من هذه البكسلات. النتيجة الإجمالية لهذا الموضع هي 28.33 × 4 = 113.33. ستكون للصورة الجديدة هذه القيمة عند (8، 11).

يتناول الرسم التخطيطي والمناقشة أعلاه ثلاثة مواضع نواة فقط. تُكرّر عملية الالتفاف هذه العملية لكل موضع نواة ممكن في الصورة. يُعطي هذا قيمة لكل موضع بكسل في الصورة الجديدة.

تظهر نتيجة الالتفاف على اليمين في الصورة التالية، مع الصورة الأصلية على اليسار:

النواة التي استخدمتها هي نواة ضبابية مربعة. معاملها 1/9، مما يجعل الوزن الإجمالي للنواة 1. نتيجة الالتفاف هي نسخة ضبابية من الصورة الأصلية. هناك نوى أخرى تؤدي وظائف مختلفة، بما في ذلك أساليب ضبابية مختلفة، وكشف الحواف، والشحذ، وغيرها.

تحتوي مكتبة Pillow على العديد من النوى والوظائف المُدمجة التي تُنفّذ عملية الالتفاف المذكورة أعلاه. لستَ بحاجة إلى فهم رياضيات التصفية عبر الالتفاف لاستخدام هذه المرشحات، ولكن من المفيد دائمًا معرفة ما يحدث خلف الكواليس عند استخدام هذه الأدوات.

ستتناول الأقسام التالية النوى وإمكانيات تصفية الصور المتوفرة في وحدة ImageFilter في Pillow.

تشويش الصورة، وتوضيحها، وتنعيمها

ستعود إلى استخدام صورة المباني التي استخدمتها في بداية هذا الدرس. يمكنك بدء جلسة REPL جديدة لهذا القسم:

>>> from PIL import Image, ImageFilter
>>> filename = "buildings.jpg"
>>> with Image.open(filename) as img:
...     img.load()
...

بالإضافة إلىImage، يمكنك أيضًا استيراد وحدة ImageFilter من Pillow. يمكنك استخدام دالة .filter() لتطبيق الترشيح على الصورة. تتطلب هذه الدالة نواة التفاف كمعامل لها، ويمكنك استخدام إحدى النوى العديدة المتوفرة في وحدة ImageFilter في Pillow. المجموعة الأولى من المرشحات التي ستتعلمها تتعامل مع تشويش الصورة، وزيادة حدتها، وتنعيمها.

يمكنك تشويش الصورة باستخدام مرشح ImageFilter.BLUR المحدد مسبقًا:

>>> blur_img = img.filter(ImageFilter.BLUR)
>>> blur_img.show()

الصورة المعروضة هي نسخة ضبابية من الصورة الأصلية. يمكنك تكبير الصورة لملاحظة الفرق بمزيد من التفصيل باستخدام دالة .crop() ثم عرض الصور مجددًا باستخدام دالة .show():

>>> img.crop((300, 300, 500, 500)).show()
>>> blur_img.crop((300, 300, 500, 500)).show()

الصورتان المقصوصتان توضحان الفرق بين الإصدارين:

يمكنك تخصيص نوع وكمية التمويه الذي تحتاجه باستخدام ()ImageFilter.BoxBlur أو ()ImageFilter.GaussianBlur:

>>> img.filter(ImageFilter.BoxBlur(5)).show()
>>> img.filter(ImageFilter.BoxBlur(20)).show()
>>> img.filter(ImageFilter.GaussianBlur(20)).show()

يمكنك رؤية الصور الثلاثة الضبابية أدناه، والتي تظهر بنفس الترتيب كما في الكود أعلاه:

مرشح .BoxBlur() مشابه للمرشح الموصوف في القسم السابق الذي تناول نوى الالتفاف. الوسيطة هي نصف قطر مرشح تمويه الصندوق. في القسم السابق الذي تناول النوى، كان مرشح تمويه الصندوق الذي استخدمته مرشحًا 3×3. هذا يعني أن نصف قطره 1، لأن المرشح يمتد بكسلًا واحدًا من المركز.

تُظهر الصور الضبابية أن مرشح ضبابية الصندوق بنصف قطر 20 ينتج صورة أكثر ضبابية من الصورة التي تم إنشاؤها بواسطة مرشح ضبابية الصندوق بنصف قطر 5.

يمكنك أيضًا استخدام مُرشِّح .GaussianBlur()، الذي يستخدم نواة تمويه غاوسي. تُعطي نواة التمويه الغوسي وزنًا أكبر للبكسلات في مركز النواة مقارنةً بتلك الموجودة في الحواف، مما يُؤدي إلى تمويه أكثر سلاسةً مما يُنتجه تمويه الصندوق. لهذا السبب، يُمكن أن يُعطي التمويه الغوسي نتائج أفضل في كثير من الحالات.

ماذا لو أردتَ تحسين صورة؟ في هذه الحالة، يمكنك استخدام مرشح ImageFilter.SHARPEN ومقارنة النتيجة بالصورة الأصلية:

>>> sharp_img = img.filter(ImageFilter.SHARPEN)
>>> img.crop((300, 300, 500, 500)).show()
>>> sharp_img.crop((300, 300, 500, 500)).show()

أنت تقارن نسخةً مقصوصةً من الصورتين تُظهر جزءًا صغيرًا من المبنى. الصورة المُحسّنة على اليمين.

ربما بدلًا من شحذ الصورة، تحتاج إلى تنعيمها. يمكنك تحقيق ذلك بتمرير ImageFilter.SMOOTH كوسيطة لـ .filter():

>>> smooth_img = img.filter(ImageFilter.SMOOTH)
>>> img.crop((300, 300, 500, 500)).show()
>>> smooth_img.crop((300, 300, 500, 500)).show()

في الأسفل، يمكنك رؤية الصورة الأصلية على اليسار والصورة الملساء على اليمين:

ستشاهد تطبيقًا لمرشح التنعيم في القسم التالي، حيث ستتعرف على المزيد من المرشحات في وحدة ImageFilter. تعمل هذه المرشحات على حواف الكائنات في الصورة.

اكتشاف الحواف وتحسينها ونقشها

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

تحتوي وحدة ImageFilter في Pillow على نواة مُعدّة مسبقًا لتحقيق ذلك. في هذا القسم، ستستخدم صورة المباني مجددًا وتحولها إلى تدرج الرمادي قبل تطبيق مرشح كشف الحواف. يمكنك متابعة جلسة REPL من القسم السابق:

>>> img_gray = img.convert("L")
>>> edges = img_gray.filter(ImageFilter.FIND_EDGES)
>>> edges.show()

النتيجة هي صورة توضح حواف الصورة الأصلية:

النتيجة هي صورة توضح حواف الصورة الأصلية:

>>> img_gray_smooth = img_gray.filter(ImageFilter.SMOOTH)
>>> edges_smooth = img_gray_smooth.filter(ImageFilter.FIND_EDGES)
>>> edges_smooth.show()

يمكنك مشاهدة مقارنة بين صورة التدرج الرمادي الأصلية ونتيجتي كشف الحواف أدناه. يظهر في الأسفل الإصدار الذي تم فيه التنعيم قبل كشف الحواف.

يمكنك أيضًا تحسين حواف الصورة الأصلية باستخدام مرشح ImageFilter.EDGE_ENHANCE:

>>> edge_enhance = img_gray_smooth.filter(ImageFilter.EDGE_ENHANCE)
>>> edge_enhance.show()

استخدمتَ النسخة المُنعَّمة من صورة التدرج الرمادي لتحسين حوافها. يظهر أدناه جزء من صورة التدرج الرمادي الأصلية والصورة المُحسَّنة حوافها جنبًا إلى جنب. تظهر الصورة المُحسَّنة حوافها على اليمين.

هناك مرشح آخر مُعَرَّف مُسبقًا في ImageFilter يُعنى بحواف الكائنات وهو ImageFilter.EMBOSS. يُمكنك تمريره كمُعامل إلى .filter() كما فعلت مع المرشحات الأخرى في هذا القسم:

>>> emboss = img_gray_smooth.filter(ImageFilter.EMBOSS)
>>> emboss.show()

أنت تستخدم النسخة الرمادية الناعمة كنقطة بداية لهذا الفلتر. يمكنك رؤية الصورة البارزة أدناه، والتي تُظهر تأثيرًا مختلفًا باستخدام حواف الصورة.

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

تقسيم الصورة وتركيبها: مثال

وهنا الصورتين:

يمكنك استخدام مكتبة بايثون Pillow لاستخراج القطة من الصورة الأولى ووضعها على أرضية فناء الدير. ستستخدم عدة تقنيات لمعالجة الصور لتحقيق ذلك.

عتبة الصورة

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

أولاً، يمكنك قص الصورة إلى صورة أصغر لإزالة بعض الخلفية. يمكنك بدء جلسة REPL جديدة لهذا المشروع:

>>> from PIL import Image
>>> filename_cat = "cat.jpg"

>>> with Image.open(filename_cat) as img_cat:
...     img_cat.load()
...

>>> img_cat = img_cat.crop((800, 0, 1650, 1281))
>>> img_cat.show()

تحتوي الصورة المقصوصة على القطة وبعض الخلفية القريبة جدًا من القطة بحيث لا يمكنك قصها:

يُمثَّل كل بكسل في صورة ملونة رقميًا بثلاثة أرقام تُقابل قيم الأحمر والأخضر والأزرق لذلك البكسل. تحديد العتبة هو عملية تحويل جميع البكسلات إلى القيمة القصوى أو الدنيا، بناءً على ما إذا كانت أعلى أو أقل من رقم معين. من الأسهل القيام بذلك على صورة بدرجات الرمادي:

>>> img_cat_gray = img_cat.convert("L")
>>> img_cat_gray.show()
>>> threshold = 100
>>> img_cat_threshold = img_cat_gray.point(
...     lambda x: 255 if x > threshold else 0
... )
>>> img_cat_threshold.show()

يمكنك تحقيق قيمة العتبة باستدعاء دالة .point() لتحويل كل بكسل في الصورة الرمادية إلى 255 أو 0. يعتمد التحويل على ما إذا كانت القيمة في الصورة الرمادية أكبر أو أصغر من قيمة العتبة. قيمة العتبة في هذا المثال هي 100.

يوضح الشكل أدناه الصورة ذات التدرج الرمادي ونتيجة عملية تحديد العتبة:

في هذا المثال، تُحوَّل جميع نقاط الصورة الرمادية التي كانت قيمة البكسل فيها أكبر من ١٠٠ إلى اللون الأبيض، وتُحوَّل جميع البكسلات الأخرى إلى اللون الأسود. يمكنك تغيير حساسية عملية تحديد العتبة بتغيير قيمة العتبة.

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

يمكنك استخراج القنوات الحمراء والخضراء والزرقاء من الصورة الملونة كما فعلت سابقًا:

>>> red, green, blue = img_cat.split()
>>> red.show()
>>> green.show()
>>> blue.show()

تظهر القنوات الحمراء والخضراء والزرقاء أدناه، من اليسار إلى اليمين. جميعها معروضة كصور بدرجات الرمادي.

تتميز القناة الزرقاء بتباين أعلى بين وحدات البكسل التي تمثل القطة وتلك التي تمثل الخلفية. يمكنك استخدام صورة القناة الزرقاء لتحديد الحد الأقصى:

>>> threshold = 57
>>> img_cat_threshold = blue.point(lambda x: 255 if x > threshold else 0)
>>> img_cat_threshold = img_cat_threshold.convert("1")
>>> img_cat_threshold.show()

في هذا المثال، استخدمتَ قيمة عتبة 57. حوّل الصورة أيضًا إلى صيغة ثنائية باستخدام “1” كمعامل لـ .convert(). لا يمكن أن تحتوي وحدات البكسل في الصورة الثنائية إلا على قيم 0 أو 1.

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

قد تحتاج إلى تعديل قيمة العتبة قليلاً إذا كانت نتائجك لا تتطابق مع النتائج الموضحة في هذا البرنامج التعليمي.

نتيجة تحديد العتبة هي ما يلي:

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

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

التآكل والتمدد

يُظهر الجانب الأيسر من هذه الصورة الثنائية نقطة بيضاء على خلفية سوداء، بينما يُظهر الجانب الأيمن ثقبًا أسود في قسم أبيض متصل.

التآكل هو عملية إزالة البكسلات البيضاء من حدود الصورة. يمكن تحقيق ذلك في صورة ثنائية باستخدام ImageFilter.MinFilter(3) كمعامل لدالة .filter(). يستبدل هذا المرشح قيمة البكسل بالقيمة الدنيا للبكسلات التسعة في المصفوفة 3×3 المحيطة بالبكسل. في الصورة الثنائية، هذا يعني أن قيمة البكسل ستكون صفرًا إذا كانت أي من البكسلات المجاورة له صفرًا.

يمكنك ملاحظة تأثير التآكل بتطبيق ImageFilter.MinFilter(3) عدة مرات على صورة dot_and_hole.jpg. يجب عليك الاستمرار بنفس جلسة REPL كما في القسم السابق:

>>> from PIL import ImageFilter
>>> filename = "dot_and_hole.jpg"

>>> with Image.open(filename) as img:
...     img.load()
...

>>> for _ in range(3):
...     img = img.filter(ImageFilter.MinFilter(3))
...

>>> img.show()

لقد طبقتَ الفلتر ثلاث مرات باستخدام حلقة for. يُعطي هذا الكود الناتج التالي:

لقد تقلصت النقطة لكن الثقب ازداد نتيجة للتآكل.

التمدد هو عملية عكسية للتآكل. تُضاف بكسلات بيضاء إلى حدود الصورة الثنائية. يمكنك تحقيق التمدد باستخدام ImageFilter.MaxFilter(3)، الذي يحوّل البكسل إلى اللون الأبيض إذا كان أيٌّ من جيرانه أبيض.

يمكنك تطبيق التوسيع على نفس الصورة التي تحتوي على نقطة وثقب، والتي يمكنك فتحها وتحميلها مرة أخرى:

>>> with Image.open(filename) as img:
...     img.load()
...

>>> for _ in range(3):
...     img = img.filter(ImageFilter.MaxFilter(3))
...

>>> img.show()

لقد أصبحت النقطة الآن أكبر، وتقلص الثقب:

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

>>> with Image.open(filename) as img:
...     img.load()
...

>>> for _ in range(10):
...     img = img.filter(ImageFilter.MinFilter(3))
...

>>> img.show()

>>> for _ in range(10):
...     img = img.filter(ImageFilter.MaxFilter(3))
...

>>> img.show()

تُنفِّذ عشر دورات تآكل باستخدام حلقة for الأولى. الصورة في هذه المرحلة هي التالية:

اختفت النقطة، وأصبح الثقب أكبر مما كان عليه في الصورة الأصلية. تُجري حلقة for الثانية عشر دورات تمدد، مما يُعيد الثقب إلى حجمه الأصلي:

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

يمكنك تحديد وظائف لأداء عدة دورات من التآكل والتمدد:

>>> def erode(cycles, image):
...     for _ in range(cycles):
...          image = image.filter(ImageFilter.MinFilter(3))
...     return image
...

>>> def dilate(cycles, image):
...     for _ in range(cycles):
...          image = image.filter(ImageFilter.MaxFilter(3))
...     return image
...

تُسهّل هذه الوظائف تجربة التآكل والتمدد في الصورة. ستستخدم هذه الوظائف في القسم التالي أثناء العمل على وضع القطة في الدير.

تقسيم الصورة باستخدام العتبة

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

بدءًا من الصورة img_cat_threshold التي حصلت عليها سابقًا، يمكنك البدء بسلسلة من التآكلات لإزالة البكسلات البيضاء التي تُمثل الخلفية في الصورة الأصلية. يُنصح بمواصلة العمل في جلسة REPL نفسها كما في الأقسام السابقة.

>>> step_1 = erode(12, img_cat_threshold)
>>> step_1.show()

لم تعد صورة العتبة المتآكلة تحتوي على بكسلات بيضاء تمثل خلفية الصورة:

مع ذلك، فإن القناع المتبقي أصغر من الشكل العام للقط، وبه ثقوب وفجوات. يمكنك إجراء توسيعات لملء الفجوات:

>>> step_2 = dilate(58, step_1)
>>> step_2.show()

ملأت دورات التمدد الثماني والخمسون جميع الثقوب في القناع لإعطاء الصورة التالية:

مع ذلك، هذا القناع كبير جدًا. لذا، يُمكنك إنهاء العملية بسلسلة من التآكلات:

>>> cat_mask = erode(45, step_2)
>>> cat_mask.show()

النتيجة هي قناع يمكنك استخدامه لتقسيم صورة القطة:

يمكنك تجنب الحواف الحادة للقناع الثنائي بتمويهه. سيتعين عليك أولاً تحويله من صورة ثنائية إلى صورة بدرجات الرمادي:

>>> cat_mask = cat_mask.convert("L")
>>> cat_mask = cat_mask.filter(ImageFilter.BoxBlur(20))
>>> cat_mask.show()

يعيد مرشح BoxBlur() القناع التالي:

يبدو القناع الآن كقطة! أنت الآن جاهز لاستخراج صورة القطة من خلفيتها:

>>> blank = img_cat.point(lambda _: 0)
>>> cat_segmented = Image.composite(img_cat, blank, cat_mask)
>>> cat_segmented.show()

أولاً، أنشئ صورة فارغة بنفس حجم img_cat. أنشئ كائن صورة جديد من img_cat باستخدام .point() وضبط جميع القيم على صفر. بعد ذلك، استخدم الدالة ()composite في PIL.Image لإنشاء صورة مكونة من img_cat وفارغة باستخدام cat_mask لتحديد أجزاء كل صورة. الصورة المركبة موضحة أدناه:

لقد قمت بتجزئة صورة القطة واستخراج القطة من خلفيتها.

تراكب الصور باستخدام ()Image.paste

يمكنك الذهاب إلى خطوة أبعد ولصق الصورة المجزأة للقط في صورة فناء الدير من مستودع الصور لهذا البرنامج التعليمي:

>>> filename_monastery = "monastery.jpg"
>>> with Image.open(filename_monastery) as img_monastery:
...     img_monastery.load()

>>> img_monastery.paste(
...     img_cat.resize((img_cat.width // 5, img_cat.height // 5)),
...     (1300, 750),
...     cat_mask.resize((cat_mask.width // 5, cat_mask.height // 5)),
... )

>>> img_monastery.show()

لقد استخدمتَ دالة .paste() للصق صورة على أخرى. يمكن استخدام هذه الطريقة بثلاثة وسيطات:

  • الحجة الأولى هي الصورة التي تريد لصقها. تقوم بتغيير حجم الصورة إلى خمس حجمها باستخدام عامل القسمة الصحيح (//).
  • الوسيطة الثانية هي الموقع في الصورة الرئيسية الذي تريد لصق الصورة الثانية فيه. تتضمن المجموعة إحداثيات الزاوية العلوية اليسرى من الصورة الرئيسية التي تريد لصقها.
  • توفر الحجة الثالثة القناع الذي ترغب في استخدامه إذا كنت لا تريد لصق الصورة بأكملها.

لقد استخدمتَ القناع الناتج عن عملية تحديد العتبة والتآكل والتمدد للصق القطة بدون خلفيتها. والنتيجة هي الصورة التالية:

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

معالجة الصور باستخدام NumPy وPillow

يحتوي Pillow على مجموعة واسعة من الدوال والمرشحات المدمجة. ومع ذلك، قد تحتاج أحيانًا إلى تحسينات إضافية في معالجة الصور تتجاوز الميزات المتوفرة في Pillow.

يمكنك معالجة الصورة بشكل أكبر باستخدام NumPy.

مكتبة بايثون شائعة جدًا للتعامل مع المصفوفات الرقمية، وهي أداة مثالية للاستخدام مع Pillow. يمكنك معرفة المزيد عن NumPy في دليل NumPy: خطواتك الأولى في علم البيانات باستخدام بايثون.

يمكنك معالجة الصورة بشكل أكبر باستخدام NumPy. NumPy مكتبة بايثون شائعة جدًا للتعامل مع المصفوفات الرقمية، وهي أداة مثالية للاستخدام مع Pillow. يمكنك معرفة المزيد عن NumPy في دليل NumPy: خطواتك الأولى في علم البيانات باستخدام بايثون.

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

(venv) $ python -m pip install numpy

الآن بعد أن قمت بتثبيت NumPy، فأنت جاهز لاستخدام Pillow وNumPy لتحديد الفرق بين صورتين.

استخدام NumPy لطرح الصور من بعضها البعض

حاول أن تلاحظ الاختلافات بين الصورتين التاليتين:

خطوتك الأولى هي قراءة الصور باستخدام Pillow وتحويلها إلى مصفوفات NumPy:

>>> import numpy as np
>>> from PIL import Image

>>> with Image.open("house_left.jpg") as left:
...     left.load()
...
>>> with Image.open("house_right.jpg") as right:
...     right.load()
...

>>> left_array = np.asarray(left)
>>> right_array = np.asarray(right)

>>> type(left_array)
<class 'numpy.ndarray'>
>>> type(right_array)
<class 'numpy.ndarray'>

بما أن left_array وright_array كائنان من نوع numpy.ndarray، يمكنك تعديلهما باستخدام جميع الأدوات المتاحة في NumPy. يمكنك طرح مصفوفة من الأخرى لإظهار البكسلات التي تختلف بين الصورتين:

>>> difference_array =  right_array - left_array
>>> type(difference_array)
<class 'numpy.ndarray'>

عند طرح مصفوفة من أخرى بنفس الحجم، ستحصل على مصفوفة أخرى بنفس شكل المصفوفات الأصلية. يمكنك تحويل هذه المصفوفة إلى صورة باستخدام ()Image.fromarray في Pillow:

>>> difference = Image.fromarray(difference_array)
>>> difference.show()

النتيجة المترتبة على طرح مجموعة NumPy من مجموعة أخرى وتحويلها إلى Image هي صورة الفرق الموضحة أدناه:

تُظهر صورة الاختلاف ثلاث مناطق فقط من الصورة الأصلية. تُبرز هذه المناطق الاختلافات بين الصورتين. كما يُمكنك رؤية بعض الضوضاء المحيطة بالغيمة والسياج، والتي تعود إلى تغييرات طفيفة في ضغط JPEG الأصلي في المنطقة المحيطة بهذه العناصر.

استخدام NumPy لإنشاء الصور

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

>>> import numpy as np
>>> from PIL import Image

>>> square = np.zeros((600, 600))
>>> square[200:400, 200:400] = 255

>>> square_img = Image.fromarray(square)
>>> square_img
<PIL.Image.Image image mode=F size=600x600 at 0x7FC7D8541F70>

>>> square_img.show()

أنشئ مصفوفة بأبعاد 600×600 تحتوي على أصفار في كل مكان. بعد ذلك، اضبط قيمة مجموعة البكسلات في مركز المصفوفة على 255.

يمكنك فهرسة مصفوفات NumPy باستخدام كلٍّ من الصفوف والأعمدة. في هذا المثال، تُمثل الشريحة الأولى، 200:400، الصفوف من 200 إلى 399. أما الشريحة الثانية، 200:400، والتي تلي الفاصلة، فتُمثل الأعمدة من 200 إلى 399.

يمكنك فهرسة مصفوفات NumPy باستخدام كلٍّ من الصفوف والأعمدة. في هذا المثال، تُمثل الشريحة الأولى، 200:400، الصفوف من 200 إلى 399. أما الشريحة الثانية، 200:400، والتي تلي الفاصلة، فتُمثل الأعمدة من 200 إلى 399.

يمكنك استخدام ()Image.fromarray لتحويل مصفوفة NumPy إلى كائن من نوع Image. يظهر أدناه ناتج الكود أعلاه:

لقد أنشأتَ صورةً بتدرج الرمادي تحتوي على مربع. يُستدل على وضع الصورة تلقائيًا عند استخدام دالة ()Image.fromarray. في هذه الحالة، يُستخدم الوضع “F”، وهو ما يُقابل صورةً ذات بكسلات فاصلة عائمة 32 بت. يمكنك تحويل هذه الصورة إلى صورة أبسط بتدرج الرمادي ذات بكسلات 8 بت إذا رغبت في ذلك:

>>> square_img = square_img.convert("L")

يمكنك أيضًا المضي قدمًا وإنشاء صورة ملونة. كرر العملية أعلاه لإنشاء ثلاث صور، واحدة للقناة الحمراء، وأخرى للقناة الخضراء، وثالثة للقناة الزرقاء.

>>> red = np.zeros((600, 600))
>>> green = np.zeros((600, 600))
>>> blue = np.zeros((600, 600))
>>> red[150:350, 150:350] = 255
>>> green[200:400, 200:400] = 255
>>> blue[250:450, 250:450] = 255

>>> red_img = Image.fromarray(red).convert("L")
>>> green_img = Image.fromarray(green).convert("L")
>>> blue_img = Image.fromarray((blue)).convert("L")

أنشئ كائن صورة من كل مصفوفة NumPy، ثم حوّل الصور إلى الوضع “L”، الذي يمثل تدرج الرمادي. الآن، يمكنك دمج هذه الصور الثلاث المنفصلة في صورة RGB واحدة باستخدام دالة ()Image.merge:

>>> square_img = Image.merge("RGB", (red_img, green_img, blue_img))
>>> square_img
<PIL.Image.Image image mode=RGB size=600x600 at 0x7FC7C817B9D0>

>>> square_img.show()

الوسيطة الأولى في ()Image.merge هي وضع إخراج الصورة. الوسيطة الثانية هي تسلسل للصور الفردية أحادية النطاق. يُنشئ هذا الكود الصورة التالية:

لقد دمجتَ الأشرطة المنفصلة في صورة ألوان RGB. في القسم التالي، ستخطو خطوةً أبعد وتُنشئ صورة GIF متحركة باستخدام NumPy وPillow.

إنشاء الرسوم المتحركة

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

>>> import numpy as np
>>> from PIL import Image

>>> square_animation = []
>>> for offset in range(0, 100, 2):
...     red = np.zeros((600, 600))
...     green = np.zeros((600, 600))
...     blue = np.zeros((600, 600))
...     red[101 + offset : 301 + offset, 101 + offset : 301 + offset] = 255
...     green[200:400, 200:400] = 255
...     blue[299 - offset : 499 - offset, 299 - offset : 499 - offset] = 255
...     red_img = Image.fromarray(red).convert("L")
...     green_img = Image.fromarray(green).convert("L")
...     blue_img = Image.fromarray((blue)).convert("L")
...     square_animation.append(
...         Image.merge(
...             "RGB",
...             (red_img, green_img, blue_img)
...         )
...     )
...

أنشئ قائمة فارغة تُسمى square_animation، والتي ستستخدمها لتخزين الصور المختلفة التي تُولّدها. داخل حلقة for، أنشئ مصفوفات NumPy للقنوات الحمراء والخضراء والزرقاء، كما فعلت في القسم السابق. المصفوفة التي تحتوي على الطبقة الخضراء هي نفسها دائمًا، وتمثل مربعًا في وسط الصورة.

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

لاحظ أنه في هذا المثال، تقوم بالتكرار على نطاق (0، 100، 2)، مما يعني أن الإزاحة المتغيرة تزداد في خطوات من اثنين.

لقد تعلمت سابقًا أنه يمكنك حفظ كائن Image في ملف باستخدام دالة ()Image.save. يمكنك استخدام الدالة نفسها لحفظ ملف GIF يحتوي على سلسلة من الصور. يمكنك استدعاء دالة ()Image.save على أول صورة في السلسلة، وهي أول صورة تُخزّن في قائمة square_animation:

>>> square_animation[0].save(
...     "animation.gif", save_all=True, append_images=square_animation[1:]
... )

الوسيطة الأولى في .save() هي اسم الملف الذي تريد حفظه. يُحدد امتداد اسم الملف .save() تنسيق الملف المطلوب لإخراجه. يمكنك أيضًا تضمين وسيطتين رئيسيتين في .save():

  • save_all=True يضمن حفظ جميع الصور في التسلسل، وليس فقط الصورة الأولى.
  • append_images=square_animation[1:] يسمح لك بإضافة الصور المتبقية في التسلسل إلى ملف GIF.

يحفظ هذا الكود ملف animation.gif في ملف، ويمكنك بعد ذلك فتح ملف GIF باستخدام أي برنامج صور. يتكرر ملف GIF افتراضيًا، ولكن في بعض الأنظمة، ستحتاج إلى إضافة الدالة loop=0 إلى .save() لضمان تكرار ملف GIF. الرسوم المتحركة التي ستحصل عليها هي التالية:

المربعات الثلاثة بألوانها المختلفة تندمج في مربع أبيض واحد. هل يمكنك إنشاء رسوم متحركة خاصة بك باستخدام أشكال وألوان مختلفة؟

لقد تعلمتَ كيفية استخدام Pillow للتعامل مع الصور ومعالجتها. إذا كنتَ تستمتع بالعمل مع الصور، فقد ترغب في التعمق في عالم معالجة الصور. هناك الكثير لتتعلمه حول نظرية وممارسة معالجة الصور. يُعد كتاب “معالجة الصور الرقمية” لغونزاليس وودز نقطة انطلاق جيدة، وهو الكتاب المرجعي الأبرز في هذا المجال.

ليست Pillow المكتبة الوحيدة التي يمكنك استخدامها في بايثون لمعالجة الصور. إذا كنت تهدف إلى إجراء بعض المعالجة الأساسية، فقد تكون التقنيات التي تعلمتها في هذا البرنامج التعليمي كافية لك. أما إذا كنت ترغب في التعمق في تقنيات معالجة الصور الأكثر تقدمًا، مثل تطبيقات التعلم الآلي والرؤية الحاسوبية، فيمكنك استخدام Pillow كخطوة أولى نحو مكتبات أخرى مثل OpenCV وscikit-image.

يمكنك الآن تصفح الصور في مجلد الصور على جهاز الكمبيوتر، واختيار بعض الصور التي يمكنك قراءتها كصور باستخدام Pillow، وتحديد كيفية معالجتها، ثم إجراء بعض عمليات المعالجة عليها. استمتع!


اكتشاف المزيد من بايثون العربي

اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

Scroll to Top

اكتشاف المزيد من بايثون العربي

اشترك الآن للاستمرار في القراءة والحصول على حق الوصول إلى الأرشيف الكامل.

Continue reading