اذهب إلى المحتوى

ستصادف ملفات الصور الرقمية طوال الوقت إذا كان لديك كاميرا رقمية أو حتى إذا رفعتَ صورًا من هاتفك على حسابك على فيسبوك أو انستغرام مثلًا، وقد تعرف كيفية استخدام برامج الرسوميات الأساسية مثل الرسام Microsoft Paint أو Paintbrush، أو حتى التطبيقات الأكثر تقدمًا مثل أدوبي فوتوشوب Adobe Photoshop، ولكن إذا كنت بحاجة إلى تعديل عددٍ كبير من الصور، فيمكن أن يكون إنجاز ذلك يدويًا مهمة طويلة ومملة.

تُعَد Pillow وحدةَ بايثون خارجية للتفاعل مع ملفات الصور، وتحتوي هذه الوحدة على العديد من الدوال التي تسهّل قص محتوى الصورة وتغيير حجمه وتعديله. يمكن لبايثون Python تعديل مئات أو آلاف الصور آليًا بسهولة بفضل القدرة على معالجة الصور باستخدام الطريقة نفسها التي تستخدمها برامجٌ مثل برنامج الرسام أو الفوتوشوب. يمكنك تثبيت وحدة Pillow من خلال تشغيل الأمر pip install --user -U pillow==9.2.0.

أساسيات الصور الحاسوبية

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

الألوان وقيم RGBA

تمثّل البرامج الحاسوبية لونًا في صورة بوصفه قيمة RGBA، والتي هي مجموعة من الأعداد التي تحدّد مقدار اللون الأحمر والأخضر والأزرق وقيمة ألفا Alpha (أو الشفافية Transparency) في اللون. كل قيمة من قيم هذه المكونات هي عددٌ صحيح قيمته من 0 (عدم وجود لون على الإطلاق) إلى 255 (الحد الأقصى). تُسنَد قيم RGBA إلى البكسلات، والبكسل Pixel هو أصغر نقطة من لون واحد يمكن أن تظهرها شاشة الحاسوب، فهناك ملايين البكسلات على الشاشة، ويعبّر إعداد RGB الخاص بالبكسل عن درجة اللون التي يجب أن يعرضها بدقة. تحتوي الصور أيضًا على قيمة ألفا لإنشاء قيم RGBA، حيث إذا عُرِضت صورة على الشاشة فوق صورة خلفية أو خلفية سطح مكتب، فإن قيمة ألفا تحدِّد مقدار الخلفية التي يمكنك رؤيتها عبر بكسل الصورة.

تُمثَّل قيم RGBA في الوحدة Pillow باستخدام مجموعة Tuple مكونة من أربع قيم صحيحة، فمثلًا يُمثَّل اللون الأحمر باستخدام المجموعة ‎(255, 0, 0, 255)‎، حيث يحتوي هذا اللون على الحد الأقصى من اللون الأحمر، ولا يحتوي على اللون الأخضر أو الأزرق، ويحتوي على الحد الأقصى من قيمة ألفا، مما يعني أنه مُعتَم Opaque تمامًا. يُمثَّل اللون الأخضر باستخدام المجموعة ‎(0, 255, 0, 255)‎، ويُمثَّل اللون الأزرق باستخدام المجموعة ‎(0, 0, 255, 255)‎، ويُمثَّل اللون الأبيض الذي هو مزيج من كل الألوان باستخدام المجموعة ‎(255, 255, 255, 255)‎، واللون الأسود الذي لا لون له هو ‎(0, 0, 0, 255)‎. إذا كانت قيمة الشفافية للون هي 0، فسيكون اللون غير مرئي، وتصبح قيم RGB غير مهمة، وبالتالي سيبدو اللون الأحمر غير المرئي مثل اللون الأسود غير المرئي.

تستخدم الوحدة Pillow أسماء الألوان المعيارية التي تستخدمها لغة HTML، حيث يوضح الجدول التالي مجموعة مختارة من أسماء الألوان المعيارية وقيم RGBA الخاصة بها:

اسم اللون قيمة RGBA الخاصة به
White (الأبيض) ‎(255, 255, 255, 255)‎
Green (الأخضر) ‎(0, 128, 0, 255)‎
Gray (الرمادي) ‎(128, 128, 128, 255)‎
Black (الأسود) ‎(0, 0, 0, 255)‎
Red (الأحمر) ‎(255, 0, 0, 255)‎
Blue (الأزرق) ‎(0, 0, 255, 255)‎
Yellow (الأصفر) ‎(255, 255, 0, 255)‎
Purple (البنفسجي) ‎(128, 0, 128, 255)‎

توفّر الوحدة Pillow الدالة ImageColor.getcolor()‎، وبالتالي لن تكون مضطرًا إلى حفظ قيم RGBA للألوان التي تريد استخدامها، وتأخذ هذه الدالة سلسلة نصية تمثّل اسم اللون كوسيطٍ أول لها والسلسلة النصية 'RGBA' كوسيطٍ ثانٍ لها، وتعيد مجموعة RGBA. أدخِل ما يلي في الصدفة التفاعلية Interactive Shell لمعرفة كيفية عمل هذه الدالة:

 >>> from PIL import ImageColor
 >>> ImageColor.getcolor('red', 'RGBA')
   (255, 0, 0, 255)
 >>> ImageColor.getcolor('RED', 'RGBA')
   (255, 0, 0, 255)
   >>> ImageColor.getcolor('Black', 'RGBA')
   (0, 0, 0, 255)
   >>> ImageColor.getcolor('chocolate', 'RGBA')
   (210, 105, 30, 255)
   >>> ImageColor.getcolor('CornflowerBlue', 'RGBA')
   (100, 149, 237, 255)

يجب أولًا استيراد الوحدة ImageColor من PIL ➊ (وليس من Pillow). السلسلة النصية لاسم اللون التي تمررها إلى الدالة ImageColor.getcolor()‎ غير حساسة لحالة الأحرف، لذا يعطي تمرير السلسلة النصية 'red' ➋ وتمرير السلسلة النصية 'RED' ➌ مجموعة RGBA نفسها، ويمكنك أيضًا تمرير أسماء ألوان غير اعتيادية مثل 'chocolate' و 'Cornflower Blue'.

تدعم الوحدة Pillow عددًا كبيرًا من أسماء الألوان من 'aliceblue' إلى 'whitesmoke'، حيث يمكنك العثور على القائمة الكاملة لأكثر من 100 اسم لون معياري في الموارد الموجودة على nostarch.

الإحداثيات والمجموعات المربعة

تُعنوَن بكسلات الصورة باستخدام إحداثيات x و y، والتي تُحدِّد موقع البكسل الأفقي والعمودي على التوالي في الصورة، وتكون نقطة الأصل Origin (أو مبدأ الإحداثيات) هي البكسل الموجود في الزاوية العلوية اليسرى من الصورة ونحدّدها بالصيغة ‎(0, 0)‎، حيث يمثل الصفر الأول الإحداثي x الذي يبدأ من الصفر عند نقطة الأصل وتزداد قيمته من اليسار إلى اليمين، ويمثل الصفر الثاني الإحداثي y الذي يبدأ من الصفر عند نقطة الأصل وتزداد قيمته نزولًا إلى أسفل الصورة. تزداد إحداثيات y باتجاه الأسفل، إذ يُعَد ذلك عكس الطريقة التي استخدمناها سابقًا لإحداثيات y في حصص الرياضيات في المدرسة. يوضح الشكل التالي كيفية عمل هذا النظام من الإحداثيات:

01_000023-معرب.png

إحداثيات x و y لصورة أبعادها 28‎×27 لأحد أنواع أجهزة تخزين البيانات القديمة

تأخذ العديد من دوال وتوابع الوحدة Pillow وسيطًا نوعه مجموعة مربعة Box Tuple، وهذا يعني أن الوحدة Pillow تتوقع مجموعةً مؤلفة من أربعة إحداثيات صحيحة تمثل منطقةً مستطيلة في الصورة، والأعداد الصحيحة الأربعة هي بالترتيب كما يلي:

  • Left: الإحداثي x للحافة اليسرى من المربع.
  • Top: الإحداثي y للحافة العلوية من المربع.
  • Right: الإحداثي x لبكسل واحد على يمين الحافة اليمنى القصوى للمربع، ويجب أن يكون هذا العدد الصحيح أكبر من العدد الصحيح الأيسر Left.
  • Bottom: الإحداثي y لبكسل واحد تحت الحافة السفلية للمربع، ويجب أن يكون هذا العدد الصحيح أكبر من العدد الصحيح العلوي Top.

لاحظ أن المربع يتضمن الإحداثيات اليسرى والعليا حتى الوصول إلى الإحداثيات اليمنى والسفلى ولكنه لا يتضمنها، فمثلًا تمثل المجموعة المربعة ‎(3, 1, 9, 6)‎ جميع البكسلات الموجودة في المربع الأسود في الشكل التالي:

02 000115

المنطقة التي تمثّلها المجموعة المربعة ‎(3, 1, 9, 6)‎

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

عرفنا كيفية عمل الألوان والإحداثيات في الوحدة Pillow، وسنستخدمها الآن لمعالجة الصور. سنستخدم الصورة التالية لجميع أمثلة الصدفة التفاعلية في هذا المقال:

03 000061

القطة زوفي Zophie

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

>>> from PIL import Image
>>> catIm = Image.open('zophie.png')

يمكنك تحميل الصورة من خلال استيراد الوحدة Image من الوحدة Pillow واستدعاء الدالة Image.open()‎، ثم تمرّر اسم ملف الصورة إلى هذه الدالة، ويمكنك بعد ذلك تخزين الصورة المُحمَّلة في المتغير CatIm. اسم الوحدة Pillow هو PIL لجعلها متوافقة مع الإصدارات السابقة من وحدةٍ أقدم اسمها Python Imaging Library، ولذلك يجب تشغيل التعليمة from PIL import Image بدلًا من التعليمة from Pillow import Image، ويجب استخدام تعليمة الاستيراد from PIL import Image بدلًا من التعليمة import PIL وفقًا للطريقة التي أعدّ بها منشئو Pillow هذه الوحدة.

إن لم يكن ملف الصورة موجودًا في مجلد العمل الحالي، فغيّر مجلد العمل إلى المجلد الذي يحتوي على ملف الصورة من خلال استدعاء الدالة os.chdir()‎:

>>> import os
>>> os.chdir('C:\\folder_with_image_file')

تعيد الدالة Image.open()‎ قيمة من نوع البيانات كائن Image، وهي الطريقة التي تمثّل بها الوحدة Pillow الصورة بوصفها قيمة بايثون. يمكنك تحميل كائن Image من ملف صورة (بأيّ صيغة) من خلال تمرير السلسلة النصية التي تمثل اسم الملف إلى الدالة Image.open()‎، ويمكن حفظ أيّ تغييرات تجريها على كائن Image في ملف صورة (بأيّ صيغة أيضًا) باستخدام التابع save()‎. تُجرَى جميع عمليات التدوير وتغيير الحجم والقص والرسم وغيرها من عمليات معالجة الصور من خلال استدعاءات التوابع الموافقة لهذه العمليات مع كائن Image.

سنفترض أنك استوردتَ الوحدة Image الخاصة بالوحدة Pillow وأن لديك صورة القطة Zophie مُخزَّنة في المتغير catIm لاختصار الأمثلة في هذا المقال. تأكّد من وجود الملف zophie.png في مجلد العمل الحالي حتى تتمكّن الدالة Image.open()‎ من العثور عليه، وإلّا فيجب تحديد المسار المطلق الكامل في وسيط السلسلة النصية للدالة Image.open()‎.

العمل مع نوع البيانات Image

يحتوي الكائن Image على العديد من السمات Attributes المفيدة التي توفر لك معلومات أساسية حول ملف الصورة الذي جرى تحميله منه مثل: عرضه وارتفاعه، واسم الملف، وصيغة الرسوميات (مثل JPEG أو GIF أو PNG).

أدخل مثلًا ما يلي في الصدفة التفاعلية:

   >>> from PIL import Image
   >>> catIm = Image.open('zophie.png')
   >>> catIm.size
 (816, 1088)
 >>> width, height = catIm.size
 >>> width
   816
 >>> height
   1088
   >>> catIm.filename
   'zophie.png'
   >>> catIm.format
   'PNG'
   >>> catIm.format_description
   'Portable network graphics'
 >>> catIm.save('zophie.jpg')

ننشئ كائن Image من ملف الصورة zophie.png ونخزّن هذا الكائن في المتغير catIm، ثم يمكننا أن نرى أن سمة الحجم size الخاصة بهذا الكائن تحتوي على مجموعةٍ Tuple تتألف من عرض الصورة وارتفاعها بالبكسل ➊. يمكننا إسناد القيم الموجودة في هذه المجموعة إلى المتغيرين width و height ➋ للوصول إلى العرض ➌ والارتفاع ➍ لوحدهما. تمثّل السمة filename اسم الملف الأصلي، وتمثّل السمات format و format_description -التي هي سلاسل نصية- صيغة الصورة للملف الأصلي، مع كون السمة format_description أكثر تفصيلًا.

أخيرًا، يؤدي استدعاء التابع save()‎ وتمرير 'zophie.jpg' إلى هذا التابع إلى حفظ صورة جديدة بالاسم zophie.jpg على قرص حاسوبك الصلب ➎. ترى الوحدة Pillow أن امتداد الملف هو ‎.jpg وتحفظ الصورة تلقائيًا بصيغة الصورة JPEG. يُفترَض أن يكون لديك الآن صورتان هما zophie.png و zophie.jpg على قرص حاسوبك الصلب، حيث يعتمد هذان الملفان على الصورة نفسها، ولكنهما غير متطابقين بسبب اختلاف صيغتيهما.

توفر الوحدة Pillow أيضًا الدالة Image.new()‎ التي تعيد كائن Image، حيث تشبه هذه الدالة إلى حدٍ كبيرالدالة Image.open()‎، باستثناء أن الصورة التي يمثلها كائن الدالة Image.new()‎ فارغة. وسطاء الدالة Image.new()‎ هي كما يلي:

  • السلسلة النصية 'RGBA' التي تضبط نمط الألوان على القيمة RGBA، إذ توجد أنماط أخرى لن نوضّحها في هذا المقال.
  • حجم الصورة الذي نمثّله بمجموعة مكونة من عددين صحيحين لعرض الصورة الجديدة وارتفاعها.
  • لون الخلفية الذي يجب أن تبدأ به الصورة، ونمثّله بمجموعة مكونة من أربعة أعداد صحيحة لقيمة RGBA، حيث يمكنك استخدام القيمة التي تعيدها الدالة ImageColor.getcolor()‎ لهذا الوسيط، ولكن تدعم الدالة Image.new()‎ بدلًا من ذلك تمرير سلسلة نصية تمثّل اسم اللون المعياري فقط.

أدخِل مثلًا ما يلي في الصدفة التفاعلية:

   >>> from PIL import Image
 >>> im = Image.new('RGBA', (100, 200), 'purple')
   >>> im.save('purpleImage.png')
 >>> im2 = Image.new('RGBA', (20, 20))
   >>> im2.save('transparentImage.png')

ننشئ كائن Image لصورة عرضها 100 بكسل وطولها 200 بكسل مع خلفية بنفسجية ➊، ثم نحفظ هذه الصورة في الملف purpleImage.png. نستدعي بعد ذلك الدالة Image.new()‎ مرةً أخرى لإنشاء كائن Image آخر مع تمرير المجموعة ‎(20, 20)‎ التي تمثّل الأبعاد دون تمرير شيء للون الخلفية ➋. يُعَد اللون الأسود غير المرئي ‎(0, 0, 0, 0)‎ هو اللون الافتراضي المُستخدَم عند عدم تحديد وسيط اللون، وبالتالي فإن الصورة الثانية لها خلفية شفافة، ثم نحفظ هذا المربع الشفاف الذي أبعاده 20×20 في الملف transparentImage.png.

قص الصور

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

أدخِل مثلًا ما يلي في الصدفة التفاعلية:

>>> from PIL import Image
>>> catIm = Image.open('zophie.png')
>>> croppedIm = catIm.crop((335, 345, 565, 560))
>>> croppedIm.save('cropped.png')

ننشئ كائن Image جديد للصورة المقصوصة، ونخزّن هذا الكائن في المتغير croppedIm، ثم نستدعي التابع save()‎ مع croppedIm لحفظ الصورة المقصوصة في الملف cropped.png. سينشَأ الملف الجديد cropped.png من الصورة الأصلية كما في الشكل التالي:

04 000003

تكون الصورة الجديدة هي الجزء المقصوص من الصورة الأصلية

نسخ ولصق الصور في صور أخرى

يعيد التابع copy()‎ كائن Image جديد يحتوي على الصورة نفسها للكائن Image الذي استدعيناه معه، ويُعَد ذلك مفيدًا إذا كنت بحاجة إلى إجراء تغييرات على الصورة ولكنك تريد الاحتفاظ بنسخة دون تغييرات من النسخة الأصلية، فمثلًا أدخِل ما يلي في الصدفة التفاعلية:

>>> from PIL import Image
>>> catIm = Image.open('zophie.png')
>>> catCopyIm = catIm.copy()

يحتوي المتغيران catIm و catCopyIm على كائني Image منفصلين، وكلاهما لهما الصورة نفسهما. أصبح لديك الآن كائن Image مُخزَّن في المتغير catCopyIm، ويمكنك الآن تعديل المتغير catCopyIm كما تريد وحفظه باسم ملف جديد، مع ترك الملف zophie.png دون تغيير. لنحاول مثلًا تعديل المتغير catCopyIm باستخدام التابع paste()‎، حيث يُستدعَى هذا التابع مع كائن Image ويلصق صورة أخرى فوقه. إذًا لنتابع مثال الصدفة من خلال لصق صورة أصغر على catCopyIm كما يلي:

>>> faceIm = catIm.crop((335, 345, 565, 560))
>>> faceIm.size
(230, 215)
>>> catCopyIm.paste(faceIm, (0, 0))
>>> catCopyIm.paste(faceIm, (400, 500))
>>> catCopyIm.save('pasted.png')

نمرّر أولًا مجموعة مربعة للمنطقة المستطيلة في الصورة zophie.png التي تحتوي على وجه القطة إلى التابع crop()‎، مما يؤدي إلى إنشاء كائن Image يمثل جزءًا مقصوصًا أبعاده 230‎×215، والذي نخزّنه في المتغير faceIm. يمكننا الآن لصق faceIm فوق catCopyIm، حيث يأخذ التابع paste()‎ وسيطين هما: كائن Image المصدَر "Source" ومجموعة من إحداثيات x و y لمكان لصق الزاوية العلوية اليسرى من كائن Image المصدر على كائن Image الرئيسي. استدعينا في مثالنا التابع paste()‎ مرتين مع catCopyIm، ومرّرنا المجموعة ‎(0, 0)‎ في المرة الأولى والمجموعة ‎(400, 500)‎ في المرة الثانية، مما يؤدي إلى لصق faceIm على catCopyIm مرتين، حيث نلصق الزاوية العلوية اليسرى من faceIm عند الإحداثيات ‎(0, 0)‎ على catCopyIm في المرة الأولى، ونلصق الزاوية العلوية اليسرى من faceIm عند الإحداثيات ‎(400, 500)‎. أخيرًا، نحفظ المتغير catCopyIm المُعدَّل في الملف pasted.png، وستبدو الصورة كما يلي:

05 000093

القطة زوفي بعد لصق وجهها مرتين

ملاحظة: لا يستخدم التابعان copy()‎ و paste()‎ في الوحدة Pillow حافظة Clipboard حاسوبك بالرغم من أن اسميهما يدلان على ذلك.

لاحظ أن التابع paste()‎ يعدّل كائن Image في المكان نفسه، أي أنه لا يعيد كائن Image جديد مع الصورة المُلصَقة، ولكن إذا أردتَ استدعاء هذا التابع مع الاحتفاظ أيضًا بالنسخة غير المُعدَّلة من الصورة الأصلية، فيجب نسخ الصورة أولًا ثم استدعاء التابع paste()‎ مع تلك النسخة.

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

   >>> catImWidth, catImHeight = catIm.size
   >>> faceImWidth, faceImHeight = faceIm.size
 >>> catCopyTwo = catIm.copy()
 >>> for left in range(0, catImWidth, faceImWidth):
         for top in range(0, catImHeight, faceImHeight):
               print(left, top)
               catCopyTwo.paste(faceIm, (left, top))
   0 0
   0 215
   0 430
   0 645
   0 860
   0 1075
   230 0
   230 215
   --snip--
   690 860
   690 1075
   >>> catCopyTwo.save('tiled.png')

06 000045

حلقات for المتداخلة المستخدمة مع التابع paste()‎ لتكرار وجه القطة

نخزّن عرض وارتفاع catIm في المتغيرين catImWidth و catImHeight، ثم ننشئ نسخة من catIm ونخزّنها في المتغير catCopyTwo ➊. أصبح لدينا الآن نسخة يمكننا لصقها، وبالتالي نبدأ بتكرار لصق faceIm على catCopyTwo، حيث يبدأ المتغير left الخاص بحلقة for الخارجية من القيمة 0 ويزداد بمقدار faceImWidth(230)‎ ➋، ويبدأ المتغير top الخاص بحلقة for الداخلية من القيمة 0 ويزداد بمقدار faceImHeight(215)‎ ➌. تعطي حلقات for المتداخلة هذه قيمًا للمتغيرين left و top للصق شبكةٍ من صور faceIm فوق كائن Image الذي هو catCopyTwo كما هو موضّح في الشكل السابق. نطبع قيم المتغيرين left و top لرؤية كيفية عمل هذه الحلقات المتداخلة، ثم نحفظ الصورة catCopyTwo المُعدَّلة في الملف tiled.png بعد اكتمال اللصق.

تغيير حجم الصورة

يُستدعَى التابع resize()‎ مع الكائن Image ويعيد كائن Image جديد مع العرض والارتفاع المُحدَّدين، حيث يقبل هذا التابع وسيطًا هو مجموعة مكونة من عددين صحيحين يمثلان العرض والارتفاع الجديدين للصورة المُعادة. أدخِل مثلًا ما يلي في الصدفة التفاعلية:

   >>> from PIL import Image
   >>> catIm = Image.open('zophie.png')
 >>> width, height = catIm.size
 >>> quartersizedIm = catIm.resize((int(width / 2), int(height / 2)))
   >>> quartersizedIm.save('quartersized.png')
 >>> svelteIm = catIm.resize((width, height + 300))
   >>> svelteIm.save('svelte.png')

نسند القيمتين الموجودتين في المجموعة catIm.size إلى المتغيرين width و height ➊، حيث يؤدي استخدام هذين المتغيرين بدلًا من catIm.size[0]‎ و catIm.size[1]‎ إلى جعل بقية الشيفرة البرمجية أكثر قابلية للقراءة.

يمرّر استدعاء التابع resize()‎ الأول القيمة int(width / 2)‎ للعرض الجديد والقيمة int(height / 2)‎ للارتفاع الجديد ➋، لذا سيكون لكائن Image الذي يعيده التابع resize()‎ نصف طول ونصف عرض الصورة الأصلية أو ربع حجم الصورة الأصلية. يقبل التابع resize()‎ الأعداد الصحيحة فقط في وسيط المجموعة الخاص به، ولذلك يجب تغليف عمليتي القسمة على 2 باستدعاء الدالة int()‎.

يحافظ تغيير الحجم على النسب نفسها للعرض والارتفاع، ولكن لا حاجة إلى أن يكون العرض والارتفاع الجديدين المُمرَّرين إلى التابع resize()‎ متناسبين مع الصورة الأصلية. يحتوي المتغير svelteIm على كائن Image له العرض الأصلي ولكن يكون ارتفاعه أكبر من الطول الأصلي بمقدار 300 بكسل ➌، مما يمنح القطة مظهرًا أرشق. لاحظ أن التابع resize()‎ لا يعدّل كائن Image ذاته، بل يعيد كائن Image جديد.

تدوير وقلب الصور

يمكن تدوير الصور باستخدام التابع rotate()‎ الذي يعيد كائن Image جديد للصورة المُدوَّرة ويترك كائن Image الأصلي دون تغيير. وسيط التابع rotate()‎ هو عدد صحيح أو عدد عشري يمثّل عدد الدرجات لتدوير الصورة بعكس اتجاه عقارب الساعة. أدخِل مثلًا ما يلي في الصدفة التفاعلية:

>>> from PIL import Image
>>> catIm = Image.open('zophie.png')
>>> catIm.rotate(90).save('rotated90.png')
>>> catIm.rotate(180).save('rotated180.png')
>>> catIm.rotate(270).save('rotated270.png')

لاحظ كيفية سَلسَلة استدعاءات التوابع من خلال استدعاء التابع save()‎ مباشرةً مع كائن Image الذي يعيده التابع rotate()‎. يؤدي الاستدعاء الأول للتابعين rotate()‎ و save()‎ إلى إنشاء كائن Image جديد يمثّل الصورة المُدوَّرة بعكس اتجاه عقارب الساعة بمقدار 90 درجة وحفظ الصورة المُدوَّرة في الملف rotated90.png، ويفعل الاستدعاءان الثاني والثالث الشيء نفسه، ولكن بمقدار 180 درجة و270 درجة. ستبدو النتائج كما يلي:

07 000139

الصورة الأصلية (على اليسار) والصورة المُدوَّرة بعكس اتجاه عقارب الساعة بمقدار 90 و 180 و 270 درجة

لاحظ أن عرض الصورة وارتفاعها يتغيران عند تدوير الصورة بمقدار 90 أو 270 درجة، ولكن إذا دوّرتَ الصورة بمقدار آخر، فستحافظ الصورة على الأبعاد الأصلية. نستخدم في نظام ويندوز Windows خلفية سوداء لملء أي فراغات ناتجة عن التدوير، كما هو موضح في الشكل الآتي، بينما نستخدم في نظام ماك macOS بكسلات شفافة لهذه الفراغات.

يحتوي التابع rotate()‎ على وسيط كلمات مفتاحية Keyword Argument اختياري هو expand، حيث يمكن ضبط هذا الوسيط على القيمة True لتكبير أبعاد الصورة لتناسب الصورة الجديدة المُدوَّرة بالكامل. أدخِل مثلًا ما يلي في الصدفة التفاعلية:

>>> catIm.rotate(6).save('rotated6.png')
>>> catIm.rotate(6, expand=True).save('rotated6_expanded.png')

يدوّر الاستدعاء الأول الصورة بمقدار 6 درجات ويحفظها في الملف rotate6.png كما هو موضَّح على يسار الشكل التالي، ويدوّر الاستدعاء الثاني الصورة بمقدار 6 درجات مع ضبط الوسيط expand على القيمة True ويحفظها في الملف rotate6_expanded.png كما هو موضَّح على يمين الشكل التالي:

08 000082

تدوير الصورة بمقدار 6 درجات تدويرًا عاديًا (على اليسار) والتدوير مع الوسيط expand=True (على اليمين)

يمكنك أيضًا قلب الصورة Mirror Flip باستخدام التابع transpose()‎، حيث يجب تمرير إما المعامل Image.FLIP_LEFT_RIGHT أو المعامل Image.FLIP_TOP_BOTTOM إلى هذا التابع. أدخِل مثلًا ما يلي في الصدفة التفاعلية:

>>> catIm.transpose(Image.FLIP_LEFT_RIGHT).save('horizontal_flip.png')
>>> catIm.transpose(Image.FLIP_TOP_BOTTOM).save('vertical_flip.png')

ينشئ التابع ‎transpose()‎‎ -مثل التابع rotate()‎- كائن Image جديد. مرّرنا في مثالنا المعامل Image.FLIP_LEFT_RIGHT إلى هذا التابع لقلب الصورة أفقيًا ثم حفظنا النتيجة في الملف horizontal_flip.png، ويمكننا قلب الصورة عموديًا من خلال تمرير Image.FLIP_TOP_BOTTOM ونحفظ النتيجة في الملف vertical_flip.png. ستبدو النتائج كما هو موضّح في الشكل التالي:

09 000030

الصورة الأصلية (على اليسار)، وقلب الصورة أفقيًا (في الوسط)، وقلب الصورة عموديًا (على اليمين)

تغيير البكسلات الفردية

يمكن استرداد لون البكسل الفردي أو ضبطه باستخدام التابعين getpixel()‎ و putpixel()‎، حيث يأخذ هذان التابعان مجموعةً تمثل إحداثيات x و y للبكسل، ويأخذ التابع putpixel()‎ أيضًا وسيطًا إضافيًا هو مجموعة تمثّل لون البكسل، فهذا الوسيط هو مجموعة RGBA مؤلفة من أربعة أعداد صحيحة أو مجموعة RGB مؤلفة من ثلاثة أعداد صحيحة. أدخل مثلًا ما يلي في الصدفة التفاعلية:

   >>> from PIL import Image
 >>> im = Image.new('RGBA', (100, 100))
 >>> im.getpixel((0, 0))
   (0, 0, 0, 0)
 >>> for x in range(100):
           for y in range(50):
             im.putpixel((x, y), (210, 210, 210))

   >>> from PIL import ImageColor
 >>> for x in range(100):
           for y in range(50, 100):
             im.putpixel((x, y), ImageColor.getcolor('darkgray', 'RGBA'))
   >>> im.getpixel((0, 0))
   (210, 210, 210, 255)
   >>> im.getpixel((0, 50))
   (169, 169, 169, 255)
   >>> im.save('putPixel.png')

ننشئ أولًا صورةً جديدة، والتي هي مربع شفاف أبعاده 100×100 ➊، ثم نستدعي التابع getpixel()‎ مع بعض الإحداثيات في هذه الصورة، مما يؤدي إلى إعادة المجموعة ‎(0, 0, 0, 0)‎ لأن الصورة شفافة ➋. يمكن تلوين البكسلات في هذه الصورة من خلال استخدام حلقات for متداخلة للمرور على جميع البكسلات في النصف العلوي من الصورة ➌ وتلوين كلّ بكسل باستخدام التابع putpixel()‎ ➍، حيث مرّرنا مجموعة RGB هي ‎(210, 210, 210)‎ باللون الرمادي الفاتح إلى التابع putpixel()‎.

لنفترض أننا نريد تلوين النصف السفلي من الصورة باللون الرمادي الداكن، ولكننا لا نعرف مجموعة RGB للون الرمادي الداكن، حيث لا يقبل التابع putpixel()‎ اسم لون معياري مثل 'darkgray'، لذلك يجب استخدام الدالة ImageColor.getcolor()‎ للحصول على مجموعة اللون المقابلة للون 'darkgray'. لنمر الآن ضمن حلقة على البكسلات الموجودة في النصف السفلي من الصورة ➎، ونمرّر القيمة المُعادة من الدالة ImageColor.getcolor()‎ إلى التابع putpixel()‎ ➏، ويجب أن يكون لدينا الآن صورة رمادية فاتحة في النصف العلوي ورمادية داكنة في النصف السفلي كما هو موضّح في الشكل التالي. يمكنك استدعاء التابع getpixel()‎ مع بعض الإحداثيات للتأكد من أن لون أيّ بكسل هو ما تتوقعه. أخيرًا، احفظ الصورة في الملف putPixel.png.

10 000121

الصورة putPixel.png

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

تطبيق عملي: إضافة شعار إلى صورة

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

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

11 000000

الشعار المراد إضافته إلى الصورة

إليك الخطوات العامة التي يجب أن يطبّقها برنامجك:

  1. تحميل صورة الشعار.
  2. المرور ضمن حلقة على جميع الملفات ذات الامتداد ‎.png و ‎.jpg الموجودة في مجلد العمل.
  3. التحقق مما إذا كانت الصورة أعرض أو أطول من 300 بكسل.
  4. إذا كان الأمر كذلك، فيجب تقليل العرض أو الارتفاع (الأكبر) إلى 300 بكسل وتقليص البعد الآخر بمقدارٍ متناسب معه.
  5. لصق صورة الشعار في زاوية الصورة.
  6. حفظ الصور المُعدَّلة في مجلد آخر.

وبالتالي يجب أن تطبّق شيفرتك البرمجية الخطوات التالية:

  1. فتح الملف catlogo.png بوصفه كائن Image.
  2. المرور ضمن حلقة على السلاسل النصية التي يعيدها التابع os.listdir('.')‎.
  3. الحصول على عرض الصورة وارتفاعها من السمة size.
  4. حساب العرض والارتفاع الجديدين للصورة التي غيّرنا حجمها.
  5. استدعاء التابع resize()‎ لتغيير حجم الصورة.
  6. استدعاء التابع paste()‎ للصق الشعار.
  7. استدعاء التابع save()‎ لحفظ التغييرات باستخدام اسم الملف الأصلي.

الخطوة الأولى: فتح صورة الشعار

افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد للمشروع، وأدخِل الشيفرة البرمجية التالية، واحفظها بالاسم resizeAndAddLogo.py:

   #! python3
   # resizeAndAddLogo.py - تغيير حجم جميع الصور الموجودة في مجلد العمل الحالي لتلائم مربعًا 
   # ‫أبعاده 300x300، ثم إضافة الصورة catlogo.png إلى الزاوية السفلية اليمنى
   import os
   from PIL import Image

 SQUARE_FIT_SIZE = 300
 LOGO_FILENAME = 'catlogo.png'

 logoIm = Image.open(LOGO_FILENAME)
 logoWidth, logoHeight = logoIm.size

   # المرور ضمن حلقة على جميع الملفات الموجودة في مجلد العمل

   # التحقق مما إذا كانت الصورة بحاجة إلى تغيير حجمها

   # حساب العرض والارتفاع الجديدين لتغيير الحجم

   # تغيير حجم الصورة

   # إضافة الشعار

   # حفظ التغييرات

سهّلنا تغيير البرنامج لاحقًا من خلال إعداد الثاثبتين SQUARE_FIT_SIZE ➊ و LOGO_FILENAME ➋ في بداية البرنامج، فلنفترض أن الشعار الذي تضيفه ليس رمزًا لقطة، أو لنفترض أنك تقلّل البعد الأكبر للصور الناتجة إلى قيمة مغايرة عن 300 بكسل، إذ يمكنك فتح الشيفرة البرمجية وتغيير تلك القيم مرة واحدة فقط باستخدام هذه الثوابت في بداية البرنامج، أو يمكنك إجراء ذلك بحيث تأخذ قيم هذه الثوابت من وسطاء سطر الأوامر. إن لم تستخدم هذه الثوابت، فيجب عليك البحث في الشيفرة البرمجية عن جميع نسخ القيم 300 و 'catlogo.png' ووضع قيمٍ أخرى لمشروعك الجديد مكانها، وبالتالي يجعل استخدام الثوابت برنامجك أعم.

تعيد الدالة Image.open()‎ كائن Image للشعار ➌. نسند القيم الواردة من السمة logoIm.size إلى المتغيرين logoWidth و logoHeight لسهولة القراءة ➍.

ملاحظة: تُعَد بقية البرنامج شيفرة هيكلية للتعليقات الموجودة في نهاية الشيفرة البرمجية السابقة حاليًا.

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

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

   #! python3
   # resizeAndAddLogo.py - تغيير حجم جميع الصور الموجودة في مجلد العمل الحالي لتلائم مربعًا 
   # ‫أبعاده 300x300، ثم إضافة الصورة catlogo.png إلى الزاوية السفلية اليمنى

   import os
   from PIL import Image

   --snip--

   os.makedirs('withLogo', exist_ok=True)
   # المرور ضمن حلقة على جميع الملفات الموجودة في مجلد العمل
 for filename in os.listdir('.'):
     if not (filename.endswith('.png') or filename.endswith('.jpg')) \
          or filename == LOGO_FILENAME:
         continue    # تخطي الملفات التي ليست صورًا وملف الشعار نفسه

     im = Image.open(filename)
       width, height = im.size

   --snip

أولًا، ينشئ استدعاء التابع os.makedirs()‎ مجلدًا بالاسم withLogo لتخزين الصور النهائية مع الشعار بدلًا من الكتابة فوق ملفات الصور الأصلية، ويمنع وسيط الكلمات المفتاحية exist_ok=True التابع os.makedirs()‎ من رفع استثناء إذا كان المجلد withLogo موجودًا مسبقًا. نمر ضمن حلقة على جميع الملفات الموجودة في مجلد العمل باستخدام التابع os.listdir('.')‎ ➊، وتتحقق تعليمة if الطويلة ➋ عبر هذه الحلقة مما إذا كانت جميع أسماء الملفات لا تنتهي بالامتداد ‎.png أو ‎.jpg، فإذا كان الأمر كذلك أو كان الملف صورة الشعار نفسه، فيجب أن تتخطاه الحلقة وتستخدم التعليمة continue ➌ للانتقال إلى الملف التالي. إذا كان اسم الملف filename ينتهي بالامتداد ‎.png أو ‎.jpg (وليس ملف الشعار)، فيمكنك فتحه بوصفه كائن Image ➍ وضبط العرض width والارتفاع height الخاصين به.

الخطوة الثالثة: تغيير حجم الصور

يجب أن يغيّر البرنامج حجم الصورة فقط إذا كان العرض أو الارتفاع أكبر من قيمة الثابت SQUARE_FIT_SIZE (أي 300 بكسل في مثالنا) فقط، لذا ضع الشيفرة البرمجية الخاصة بتغيير الحجم ضمن تعليمة if التي تتحقق من متغيرات العرض width والارتفاع height. إذًا أضِف الشيفرة البرمجية التالية إلى برنامجك:

#! python3
# resizeAndAddLogo.py - تغيير حجم جميع الصور الموجودة في مجلد العمل الحالي لتلائم مربعًا 
# ‫أبعاده 300x300، ثم إضافة الصورة catlogo.png إلى الزاوية السفلية اليمنى

import os
from PIL import Image

--snip--

     # التحقق مما إذا كانت الصورة بحاجة إلى تغيير حجمها
     if width > SQUARE_FIT_SIZE and height > SQUARE_FIT_SIZE:
         # حساب العرض والارتفاع الجديدين لتغيير الحجم
         if width > height:
             height = int((SQUARE_FIT_SIZE / width) * height)
               width = SQUARE_FIT_SIZE
         else:
             width = int((SQUARE_FIT_SIZE / height) * width)
               height = SQUARE_FIT_SIZE

           # تغيير حجم الصورة
           print('Resizing %s...' % (filename))
         im = im.resize((width, height))

--snip

إذا كانت الصورة بحاجة إلى تغيير حجمها، فيجب معرفة ما إذا كان عرض الصورة أو ارتفاعها أكبر من قيمة الثابت SQUARE_FIT_SIZE (أي 300 بكسل في مثالنا)، وإذا كان العرض width أكبر من الارتفاع height، فيجب تقليل الارتفاع بمقدار النسبة نفسها لتقليل العرض ➊، وهذه النسبة هي قيمة الثابت SQUARE_FIT_SIZE مقسومة على العرض الحالي، وتساوي قيمة الارتفاع height الجديدة النسبة مضروبة بقيمة الارتفاع height الحالية. يعيد معامل القسمة قيمة عشرية، ولكن يتطلب التابع resize()‎ أن تكون الأبعاد أعدادًا صحيحة، لذا تذكّر تحويل النتيجة إلى عدد صحيح باستخدام الدالة int()‎. أخيرًا، ستُضبَط قيمة العرض width الجديدة على قيمة الثابت SQUARE_FIT_SIZE.

إذا كان الارتفاع height أكبر أو يساوي العرض width (عالجنا كلتا الحالتين في تعليمة else)، فستُجرَى العملية الحسابية نفسها باستثناء التبديل بين المتغيرين height و width ➋.

سيحتوي المتغيران width و height على أبعاد الصورة الجديدة، لذا مرّرهما إلى التابع resize()‎ وخزّن كائن Image المُعاد في المتغير im ➌.

الخطوة الرابعة: إضافة الشعار وحفظ التغييرات

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

12_000011-معرب.png

يجب أن تكون الإحداثيات اليسرى والعلوية لوضع الشعار في الزاوية السفلية اليمنى هي عرض/ارتفاع الصورة مطروحًا منه عرض/ارتفاع الشعار

يجب أن تحفظ شيفرتك البرمجية كائن Image المُعدَّل بعد لصق الشعار في الصورة. إذًا أضِف ما يلي إلى برنامجك:

#! python3
# resizeAndAddLogo.py - تغيير حجم جميع الصور الموجودة في مجلد العمل الحالي لتلائم مربعًا 
# ‫أبعاده 300x300، ثم إضافة الصورة catlogo.png إلى الزاوية السفلية اليمنى

import os
from PIL import Image

--snip--

     # التحقق مما إذا كانت الصورة بحاجة إلى تغيير حجمها
     --snip--

     # إضافة الشعار
   print('Adding logo to %s...' % (filename))
   im.paste(logoIm, (width - logoWidth, height - logoHeight), logoIm)

     # حفظ التغييرات
   im.save(os.path.join('withLogo', filename))

تطبع الشيفرة البرمجية الجديدة رسالة تخبر المستخدم بإضافة الشعار ➊، وتلصق logoIm على im عند الإحداثيات المحسوبة ➋، وتحفظ التغييرات في اسم ملف ضمن المجلد withLogo ➌. سيبدو الخرج كما يلي عند تشغيل هذا البرنامج مع الملف zophie.png بوصفه الصورة الوحيدة في مجلد العمل:

Resizing zophie.png...
Adding logo to zophie.png…

سنغيّر الصورة zophie.png إلى صورة بحجم ‎225×300 بكسل تشبه الشكل التالي، وتذكّر أن التابع paste()‎ لن يلصق البكسلات الشفافة إن لم تمرّر logoIm إلى الوسيط الثالث للتابع paste()‎. يمكن لهذا البرنامج تغيير حجم مئات الصور وإضافة شعار إليها تلقائيًا في بضع دقائق فقط.

13 000106

غيّرنا حجم الصورة zophie.png وأضفنا الشعار (على اليسار).

إذا نسيت الوسيط الثالث، فستُنسَخ البكسلات الشفافة في الشعار بوصفها بكسلات بيضاء (على اليمين)

أفكار لبرامج مماثلة

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

  • إضافة نص أو عنوان URL لموقع ويب إلى الصور.
  • إضافة علامات زمنية Timestamps إلى الصور.
  • نسخ الصور أو نقلها إلى مجلدات مختلفة بناءً على أحجامها.
  • إضافة علامة مائية شفافة تقريبًا إلى الصورة لمنع الآخرين من نسخها.

الرسم على الصور

إذا أردتَ رسم خطوط أو مستطيلات أو دوائر أو أشكال بسيطة أخرى على صورة ما، فاستخدم الوحدة ImageDraw الخاصة بوحدة Pillow. أدخِل مثلًا ما يلي في الصدفة التفاعلية:

>>> from PIL import Image, ImageDraw
>>> im = Image.new('RGBA', (200, 200), 'white')
>>> draw = ImageDraw.Draw(im)

نستورد أولًا الوحدتين Image و ImageDraw، ثم ننشئ صورة جديدة، والتي هي صورة بيضاء أبعادها 200‎×200 ، ونخزّن كائن Image في المتغير im. نمرّر كائن Image إلى الدالة ImageDraw.Draw()‎ للحصول على كائن ImageDraw، حيث يحتوي هذا الكائن على عدة توابع لرسم الأشكال والنصوص على كائن Image. نخزّن بعد ذلك كائن ImageDraw في المتغير draw حتى نتمكّن من استخدامه بسهولة في المثال التالي.

رسم الأشكال

ترسم توابع الكائن ImageDraw التالية أنواعًا مختلفة من الأشكال على الصورة، وتُعَد معاملات fill و outline لهذه التوابع اختيارية وستُضبَط افتراضيًا على اللون الأبيض إذا تُرِكت دون تحديد.

رسم النقاط

يرسم التابع point(xy, fill)‎ بكسلات فردية، حيث يمثل الوسيط xy قائمةً من النقاط التي تريد رسمها، إذ يمكن أن تكون هذه القائمة قائمةً بمجموعات Tuples إحداثيات x و y مثل ‎[(x, y), (x, y), ...]‎، أو قائمة بإحداثيات x و y بدون مجموعات مثل ‎[x1, y1, x2, y2, ...]‎. يمثّل الوسيط fill لون النقاط وهو إما مجموعة RGBA أو سلسلة نصية لاسم اللون مثل 'red'، ويُعَد هذا الوسيط اختياريًا.

رسم الخطوط

يرسم التابع line(xy, fill, width)‎ خطًا أو سلسلةً من الخطوط، حيث يمثّل الوسيط xy إما قائمة من المجموعات مثل ‎[(x, y), (x, y), ...]‎، أو قائمةً من الأعداد الصحيحة مثل ‎[x1, y1, x2, y2, ...]‎، وتُعَد كلّ نقطة واحدةً من النقاط المتصلة على الخطوط التي ترسمها. يمثّل الوسيط fill الاختياري لون الخطوط باستخدام اسم اللون أو مجموعة RGBA، ويمثّل الوسيط width الاختياري عرض الخطوط وتكون قيمته الافتراضية 1 إذا تُرِك دون تحديد.

رسم المستطيلات

يرسم التابع rectangle(xy, fill, outline)‎ مستطيلًا، حيث يمثّل الوسيط xy مجموعة مربعة وفق الصيغة (left, top, right, bottom)، إذ تحدّد القيم left و top إحداثيات x و y للزاوية العلوية اليسرى للمستطيل، بينما تحدد القيم right و bottom الزاوية السفلية اليمنى. يمثّل الوسيط الاختياري fill اللونَ الذي سيملأ الجزء الداخلي من المستطيل، ويمثّل الوسيط الاختياري outline لون المخطط المحيط بالمستطيل.

رسم الأشكال البيضاوية

يرسم التابع ellipse(xy, fill, outline)‎ شكلًا بيضاويًا، وإذا كان عرض وارتفاع هذا الشكل متطابقين، فسيرسم هذا التابع دائرة. يمثّل الوسيط xy المجموعة المربعة (left, top, right, bottom) التي تمثّل مربعًا يحتوي على الشكل البيضاوي بدقة، ويمثّل الوسيط الاختياري fill لون الجزء الداخلي من الشكل البيضاوي، ويمثل الوسيط الاختياري outline لون المخطط المحيط بالشكل البيضاوي.

رسم المضلعات

يرسم التابع polygon(xy, fill, outline)‎ مضلعًا عشوائيًا، حيث يمثّل الوسيط xy قائمةً من المجموعات مثل ‎[(x, y), (x, y), ...]‎ أو أعدادًا صحيحة مثل ‎[x1, y1, x2, y2, ...]‎، والتي تمثّل النقاط التي تربط أضلاع المضلع، ويُربَط الزوج الأخير من الإحداثيات بالزوج الأول تلقائيًا. يمثل الوسيط الاختياري fill لون الجزء الداخلي من المضلع، ويمثّل الوسيط الاختياري outline لون المخطط المحيط بالمضلع.

تطبيق عملي لرسم الأشكال

أدخِل مثلًا ما يلي في الصدفة التفاعلية:

   >>> from PIL import Image, ImageDraw
   >>> im = Image.new('RGBA', (200, 200), 'white')
   >>> draw = ImageDraw.Draw(im)
 >>> draw.line([(0, 0), (199, 0), (199, 199), (0, 199), (0, 0)], fill='black')
 >>> draw.rectangle((20, 30, 60, 60), fill='blue')
 >>> draw.ellipse((120, 30, 160, 60), fill='red')
 >>> draw.polygon(((57, 87), (79, 62), (94, 85), (120, 90), (103, 113)),
   fill='brown')
 >>> for i in range(100, 200, 10):
           draw.line([(i, 0), (200, i - 100)], fill='green')

   >>> im.save('drawing.png')

ننشئ كائن Image لصورة بيضاء أبعادها 200‎×200، ونمرّره إلى الدالة ImageDraw.Draw()‎ للحصول على كائن ImageDraw الذي نخزّنه في المتغير draw، حيث يمكنك استدعاء توابع الرسم مع هذا المتغير. رسمنا مخططًا رفيعًا باللون الأسود عند حواف الصورة ➊، ومستطيلًا أزرق تكون زاويته العلوية اليسرى عند النقطة ‎(20, 30)‎ وزاويته السفلية اليمنى عند النقطة ‎(60, 60)‎ ➋، وشكلًا بيضاويًا باللون الأحمر نحدّده باستخدام مربعٍ من النقطة ‎(120, 30)‎ إلى النقطة ‎(160, 60)‎ ➌، ومضلعًا بنيًا بخمس نقاط ➍، ونمطًا Pattern من الخطوط الخضراء مرسومة باستخدام حلقة for ➎. سيبدو الملف drawing.png الناتج كما يلي:

14 000051

صورة drawing.png الناتجة

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

رسم النصوص

يحتوي كائن ImageDraw أيضًا على التابع text()‎ لرسم النصوص على الصور، حيث يأخذ هذا التابع أربعة وسطاء هي: xy و text و fill و font:

  • الوسيط xy هو مجموعة مكونة من عددين صحيحين تحدّد الزاوية العلوية اليسرى من مربع النص.
  • الوسيط text هو سلسلة النص الذي تريد كتابته.
  • الوسيط fill الاختياري هو لون النص.
  • الوسيط font الاختياري هو كائن ImageFont، حيث يُستخدَم هذا الوسيط لضبط خط النص وحجمه (سنوضّح هذا الوسيط بمزيد من التفصيل في القسم التالي).

من الصعب معرفة حجم كتلة النص التي لها خط معين مسبقًا، لذا توفّر الوحدة ImageDraw التابع textsize()‎، ووسيطه الأول هو سلسلة النص الذي تريد قياس حجمه، والوسيط الثاني هو كائن ImageFont اختياري. يعيد التابع textsize()‎ مجموعة مكونة من عددين صحيحين تمثّل العرض والارتفاع الذي سيكون عليه النص الذي له خطٌ محدّد إذا كان مكتوبًا على الصورة، حيث يمكنك استخدام هذا العرض والارتفاع لمساعدتك في حساب المكان الذي تريد وضع النص فيه على صورتك.

تُعَد الوسطاء الثلاثة الأولى للتابع text()‎ واضحةً، ولكن لنلقِ نظرة على الوسيط الرابع الاختياري كائن ImageFont قبل أن نستخدم التابع text()‎ لرسم نص على صورة.

يأخذ كل من التابعين text()‎ و textsize()‎ كائن ImageFont اختياري بوصفه الوسيط الأخير لهما. لننشئ أحد هذه الكائنات من خلال تشغيل التعليمة التالية:

>>> from PIL import ImageFont

استوردنا الوحدة ImageFont الخاصة بوحدة Pillow، ويمكننا الآن استدعاء الدالة ImageFont.truetype()‎ التي تأخذ وسيطين. الوسيط الأول هو سلسلة نصية لملف TrueType الخاص بالخط، وهو ملف الخط الفعلي الموجود على قرص حاسوبك الصلب، ويكون لملف TrueType الامتداد ‎.ttf، حيث يمكن العثور عليه في المجلدات التالية:

  • على نظام ويندوز: C:\Windows\Fonts.
  • على نظام ماك: ‎/Library/Fonts و ‎/System/Library/Fonts.
  • على نظام لينكس: ‎/usr/share/fonts/truetype.

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

الوسيط الثاني للدالة ImageFont.truetype()‎ هو عدد صحيح يمثّل حجم الخط بالنقاط بدلًا من البكسلات. ضع في بالك أن الوحدة Pillow تنشئ صور PNG بكثافة 72 بكسلًا لكل بوصة افتراضيًا، والنقطة هي 1/72 من البوصة.

أدخِل مثلًا ما يلي في الصدفة التفاعلية مع وضع اسم المجلد الفعلي الذي يستخدمه نظام تشغيلك مكان الثابت FONT_FOLDER:

   >>> from PIL import Image, ImageDraw, ImageFont
   >>> import os
 >>> im = Image.new('RGBA', (200, 200), 'white')
 >>> draw = ImageDraw.Draw(im)
 >>> draw.text((20, 150), 'Hello', fill='purple')
   >>> fontsFolder = 'FONT_FOLDER' # مثلًا ‘/Library/Fonts'
 >>> arialFont = ImageFont.truetype(os.path.join(fontsFolder, 'arial.ttf'), 32)
 >>> draw.text((100, 150), 'Howdy', fill='gray', font=arialFont)
   >>> im.save('text.png')

نستورد الوحدات Image و ImageDraw و ImageFont و os، ثم ننشئ كائن Image لصورة بيضاء جديدة أبعادها 200‎×200 ➊، وننشئ كائن ImageDraw من الكائن Image ➋. نستخدم التابع text()‎ لرسم النص "Hello" عند النقطة ‎(20, 150)‎ باللون البنفسجي ➌. لم نمرّر الوسيط الرابع الاختياري في استدعاء التابع text()‎، لذا لم نخصّص خط وحجم هذا النص.

يمكن ضبط خط وحجم النص من خلال تخزين اسم المجلد (مثل ‎/Library/Fonts) في المتغير fontsFolder، ثم نستدعي الدالة ImageFont.truetype()‎، ونمرر إليها ملف ‎.ttf للخط الذي نريده متبوعًا بعدد صحيح يمثّل حجم الخط ➍. نخزّن الكائن Font الذي نحصل عليه من الدالة ImageFont.truetype()‎ في المتغير arialFont مثلًا، ثم نمرّر هذا المتغير إلى التابع text()‎ في وسيط الكلمات المفتاحية الأخير. يرسم استدعاء التابع text()‎ ➎ النص "Howdy" عند النقطة ‎(100, 150)‎ باللون الرمادي بخط Arial وبحجم 32 نقطة.

سيبدو الملف text.png الناتج كما في الشكل التالي:

15 000146

صورة text.png الناتجة

مشاريع للتدريب

حاول كتابة البرامج التي تؤدي المهام التي سنوضّحها فيما يلي لكسب خبرة عملية أكبر.

توسيع وإصلاح برامج تطبيقنا العملي

يعمل برنامج resizeAndAddLogo.py الموجود في هذا المقال مع ملفات PNG و JPEG، ولكن تدعم الوحدة Pillow العديد من صيغ الصور الأخرى، إذًا لنوسّع هذا البرنامج لمعالجة صور GIF و BMP أيضًا. توجد مشكلة صغيرة هي أن البرنامج لا يعدّل ملفات PNG و JPEG إلّا إذا كانت امتدادات الملفات الخاصة بها بأحرف صغيرة، فمثلًا سيعالج هذا البرنامج الملف zophie.png ولن يعالج الملف zophie.PNG، لذا عدّل الشيفرة البرمجية بحيث يكون التحقق من امتداد الملف غير حساس لحالة الأحرف.

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

16 000005

ستبدو النتائج غير جميلة عندما لا تكون الصورة أكبر بكثير من الشعار

تحديد مجلدات الصور الموجودة على القرص الصلب

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

حاول كتابة برنامج يمر على كل مجلد موجودٍ على قرص حاسوبك الصلب ويعثر على مجلدات الصور المُحتملة، لذا يجب عليك أولًا تحديد ما يعنيه مجلد الصور، إذًا لنفترض أن مجلد الصور هو أيّ مجلد أكثر من نصف ملفاته صور، ولكن يجب أيضًا تحديد ما هي ملفات الصور، حيث يجب أن يكون لملف الصورة الامتداد ‎.png أو ‎.jpg. تُعَد الصور الرقمية صورًا كبيرة، إذ يجب أن يكون عرض وارتفاع ملف الصورة أكبر من 500 بكسل، فمعظم صور الكاميرا الرقمية يبلغ عرضها وارتفاعها عدة آلاف من البكسلات.

إليك شيفرة هيكلية تقريبية لما قد يبدو عليه هذا البرنامج:

#! python3
# استيراد الوحدات وكتابة التعليقات لوصف هذا البرنامج

for foldername, subfolders, filenames in os.walk('C:\\'):
    numPhotoFiles = 0
    numNonPhotoFiles = 0
    for filename in filenames:
        # ‫التحقق مما إذا كان امتداد الملف ليس ‎.png أو ‎.jpg
        if TODO:
            numNonPhotoFiles += 1
            continue    # الانتقال إلى اسم الملف التالي

        # ‫فتح ملف الصورة باستخدام الوحدة Pillow

        # التحقق مما إذا كان العرض والارتفاع أكبر من 500
        if TODO:
            # الصورة كبيرة بما يكفي لعدّها صورة
            numPhotoFiles += 1
        else:
            # الصورة صغيرة جدًا بحيث لا يمكن عدّها صورة
            numNonPhotoFiles += 1

    # إذا كان أكثر من نصف الملفات هي صور، فاطبع المسار المطلق للمجلد
    if TODO:
        print(TODO)

يجب أن يطبع البرنامج عند تشغيله المسار المطلق لأي مجلدات صور على الشاشة.

برنامج لإنشاء بطاقات جلوس مخصصة

أنجزنا في مقالٍ سابق مشروعًا تدريبيًا لإنشاء دعوات مخصصة لقائمة من الضيوف موجودة في ملف نص عادي، لذا أضِف على هذا المشروع لإنشاء صور لبطاقات الجلوس المخصصة لضيوفك باستخدام الوحدة pillow. أنشئ ملف صورة باسم الضيف وبعض زخارف الزهور لكل من الضيوف المدرجين في الملف guests.txt الذي يتوفّر على الموارد الموجودة على موقع nostarch، وتتوفر أيضًا صورة زهرة ذات ملكية عامة دون حقوق نشر في هذه الموارد.

أضِف مستطيلًا أسود على حواف صورة الدعوة بحيث تكون كدليل للقص عند طباعة الصورة للتأكد من أن جميع بطاقات جلوس لها الحجم نفسه. تُضبَط ملفات PNG التي تنتجها وحدة Pillow على 72 بكسلًا لكل بوصة، لذا تتطلب البطاقة التي أبعادها 4‎×5 بوصة صورةً بحجم 288‎×360 بكسل.

الخلاصة

تتكون الصور من مجموعة من البكسلات، وكل بكسل له قيمة RGBA للون الخاص به ويمكن تحديد مكانه باستخدام إحداثيات x و y، وهناك صيغتان شائعتان للصور هما JPEG و PNG، حيث يمكن لوحدة pillow التعامل مع هذه الصيغ للصور وغيرها من الصيغ.

إذا حمّلنا صورة إلى كائن Image، فستُخزَّن أبعاد العرض والارتفاع الخاصة بها بوصفها مجموعة مكونة من عددين صحيحين في السمة size. تمتلك كائنات نوع البيانات Image أيضًا توابعًا لمعالجة الصور الشائعة وهي: crop()‎ و copy()‎ و paste()‎ و resize()‎ و rotate()‎ و transpose()‎. يمكنك حفظ كائن Image في ملف صورة من خلال استدعاء التابع save()‎.

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

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

ترجمة -وبتصرُّف- للمقال Manipulating Images لصاحبه Al Sweigart.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...