ستصادف ملفات الصور الرقمية طوال الوقت إذا كان لديك كاميرا رقمية أو حتى إذا رفعتَ صورًا من هاتفك على حسابك على فيسبوك أو انستغرام مثلًا، وقد تعرف كيفية استخدام برامج الرسوميات الأساسية مثل الرسام 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 في حصص الرياضيات في المدرسة. يوضح الشكل التالي كيفية عمل هذا النظام من الإحداثيات:
إحداثيات x و y لصورة أبعادها 28×27 لأحد أنواع أجهزة تخزين البيانات القديمة
تأخذ العديد من دوال وتوابع الوحدة Pillow وسيطًا نوعه مجموعة مربعة Box Tuple، وهذا يعني أن الوحدة Pillow تتوقع مجموعةً مؤلفة من أربعة إحداثيات صحيحة تمثل منطقةً مستطيلة في الصورة، والأعداد الصحيحة الأربعة هي بالترتيب كما يلي:
- Left: الإحداثي x للحافة اليسرى من المربع.
- Top: الإحداثي y للحافة العلوية من المربع.
- Right: الإحداثي x لبكسل واحد على يمين الحافة اليمنى القصوى للمربع، ويجب أن يكون هذا العدد الصحيح أكبر من العدد الصحيح الأيسر Left.
- Bottom: الإحداثي y لبكسل واحد تحت الحافة السفلية للمربع، ويجب أن يكون هذا العدد الصحيح أكبر من العدد الصحيح العلوي Top.
لاحظ أن المربع يتضمن الإحداثيات اليسرى والعليا حتى الوصول إلى الإحداثيات اليمنى والسفلى ولكنه لا يتضمنها، فمثلًا تمثل المجموعة المربعة (3, 1, 9, 6)
جميع البكسلات الموجودة في المربع الأسود في الشكل التالي:
المنطقة التي تمثّلها المجموعة المربعة (3, 1, 9, 6)
معالجة الصور باستخدام الوحدة Pillow
عرفنا كيفية عمل الألوان والإحداثيات في الوحدة Pillow، وسنستخدمها الآن لمعالجة الصور. سنستخدم الصورة التالية لجميع أمثلة الصدفة التفاعلية في هذا المقال:
القطة زوفي 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 من الصورة الأصلية كما في الشكل التالي:
تكون الصورة الجديدة هي الجزء المقصوص من الصورة الأصلية
نسخ ولصق الصور في صور أخرى
يعيد التابع 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، وستبدو الصورة كما يلي:
القطة زوفي بعد لصق وجهها مرتين
ملاحظة: لا يستخدم التابعان 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')
حلقات 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 درجة. ستبدو النتائج كما يلي:
الصورة الأصلية (على اليسار) والصورة المُدوَّرة بعكس اتجاه عقارب الساعة بمقدار 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 كما هو موضَّح على يمين الشكل التالي:
تدوير الصورة بمقدار 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. ستبدو النتائج كما هو موضّح في الشكل التالي:
الصورة الأصلية (على اليسار)، وقلب الصورة أفقيًا (في الوسط)، وقلب الصورة عموديًا (على اليمين)
تغيير البكسلات الفردية
يمكن استرداد لون البكسل الفردي أو ضبطه باستخدام التابعين 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.
الصورة putPixel.png
لا يُعَد رسم بكسل واحد في كل مرة على الصورة أمرًا مريحًا للغاية، فإذا كنت بحاجة إلى رسم الأشكال، فاستخدم دوال الوحدة ImageDraw
التي سنوضّحها لاحقًا.
تطبيق عملي: إضافة شعار إلى صورة
لنفترض أن لديك مهمة مملة تتمثل في تغيير حجم آلاف الصور وإضافة شعار صغير يمثل علامة مائية في زاوية كل من هذه الصور، إذ قد يستغرق ذلك الأمر وقتًا طويلًا باستخدام برنامج رسوميات أساسي مثل برنامج الرسام Paint أو Paintbrush. يمكن لتطبيق رسوميات أكثر تقدمًا مثل برنامج الفوتوشوب Photoshop إنجاز معالجةٍ لمجموعة من الصور، ولكنه يكلف مئات الدولارات. إذًا لنكتب سكربتًا ينجز هذه المهمة نيابةً عنك.
لنفترض أن الشكل التالي هو الشعار الذي تريد إضافته إلى الزاوية اليمنى السفلية من كل صورة، وهذا الشعار هو رمزٌ لقطة سوداء ذات حدود بيضاء مع جعل بقية الصورة شفافة:
الشعار المراد إضافته إلى الصورة
إليك الخطوات العامة التي يجب أن يطبّقها برنامجك:
- تحميل صورة الشعار.
-
المرور ضمن حلقة على جميع الملفات ذات الامتداد
.png
و.jpg
الموجودة في مجلد العمل. - التحقق مما إذا كانت الصورة أعرض أو أطول من 300 بكسل.
- إذا كان الأمر كذلك، فيجب تقليل العرض أو الارتفاع (الأكبر) إلى 300 بكسل وتقليص البعد الآخر بمقدارٍ متناسب معه.
- لصق صورة الشعار في زاوية الصورة.
- حفظ الصور المُعدَّلة في مجلد آخر.
وبالتالي يجب أن تطبّق شيفرتك البرمجية الخطوات التالية:
-
فتح الملف catlogo.png بوصفه كائن
Image
. -
المرور ضمن حلقة على السلاسل النصية التي يعيدها التابع
os.listdir('.')
. -
الحصول على عرض الصورة وارتفاعها من السمة
size
. - حساب العرض والارتفاع الجديدين للصورة التي غيّرنا حجمها.
-
استدعاء التابع
resize()
لتغيير حجم الصورة. -
استدعاء التابع
paste()
للصق الشعار. -
استدعاء التابع
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
➌.
الخطوة الرابعة: إضافة الشعار وحفظ التغييرات
يجب لصق الشعار في الزاوية السفلية اليمنى سواء تغيّر حجم الصورة أم لا، ويعتمد المكان الذي يجب لصق الشعار فيه على حجم الصورة وحجم الشعار، حيث يوضح الشكل التالي كيفية حساب موضع اللصق. سيكون الإحداثي الأيسر لمكان لصق الشعار هو عرض الصورة مطروحًا منه عرض الشعار، وسيكون الإحداثي العلوي لمكان لصق الشعار هو ارتفاع الصورة مطروحًا منه ارتفاع الشعار.
يجب أن تكون الإحداثيات اليسرى والعلوية لوضع الشعار في الزاوية السفلية اليمنى هي عرض/ارتفاع الصورة مطروحًا منه عرض/ارتفاع الشعار
يجب أن تحفظ شيفرتك البرمجية كائن 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()
. يمكن لهذا البرنامج تغيير حجم مئات الصور وإضافة شعار إليها تلقائيًا في بضع دقائق فقط.
غيّرنا حجم الصورة 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 الناتج كما يلي:
صورة 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 الناتج كما في الشكل التالي:
صورة text.png الناتجة
مشاريع للتدريب
حاول كتابة البرامج التي تؤدي المهام التي سنوضّحها فيما يلي لكسب خبرة عملية أكبر.
توسيع وإصلاح برامج تطبيقنا العملي
يعمل برنامج resizeAndAddLogo.py الموجود في هذا المقال مع ملفات PNG و JPEG، ولكن تدعم الوحدة Pillow العديد من صيغ الصور الأخرى، إذًا لنوسّع هذا البرنامج لمعالجة صور GIF و BMP أيضًا. توجد مشكلة صغيرة هي أن البرنامج لا يعدّل ملفات PNG و JPEG إلّا إذا كانت امتدادات الملفات الخاصة بها بأحرف صغيرة، فمثلًا سيعالج هذا البرنامج الملف zophie.png ولن يعالج الملف zophie.PNG، لذا عدّل الشيفرة البرمجية بحيث يكون التحقق من امتداد الملف غير حساس لحالة الأحرف.
أخيرًا، يُفترَض أن يكون الشعار المُضاف إلى الزاوية السفلية اليمنى مجرد علامة صغيرة، ولكن إذا كانت الصورة بحجم الشعار نفسه تقريبًا، فستبدو النتيجة كما في الشكل التالي، لذا عدّل البرنامج resizeAndAddLogo.py بحيث يجب أن يكون عرض وارتفاع الصورة على الأقل ضعف عرض وارتفاع صورة الشعار قبل لصقه، وإلّا فيجب تخطي إضافة الشعار.
ستبدو النتائج غير جميلة عندما لا تكون الصورة أكبر بكثير من الشعار
تحديد مجلدات الصور الموجودة على القرص الصلب
قد تنقل الملفات من الكاميرا الرقمية إلى مجلدات مؤقتة في مكانٍ ما على القرص الصلب ثم تنسى هذه المجلدات، لذا من الجيد كتابة برنامج يمكنه فحص القرص الصلب بأكمله والعثور على مجلدات الصور التي نسيت مكانها.
حاول كتابة برنامج يمر على كل مجلد موجودٍ على قرص حاسوبك الصلب ويعثر على مجلدات الصور المُحتملة، لذا يجب عليك أولًا تحديد ما يعنيه مجلد الصور، إذًا لنفترض أن مجلد الصور هو أيّ مجلد أكثر من نصف ملفاته صور، ولكن يجب أيضًا تحديد ما هي ملفات الصور، حيث يجب أن يكون لملف الصورة الامتداد .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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.