5.4. قراءة البكسلات وكتابتها¶
تُخفي معظم العمليات على الصورة عملها على مستوى كل بكسل داخل استدعاء طريقة واحدة، حيث تجري الحلقات التي تلمس كل بكسل بالسرعة الأصلية (native). هناك حالات، مع ذلك، يريد فيها برنامج التطبيق لمس بكسل محدد واحد مباشرة: لقراءة ما في موضع معين، أو لكتابة قيمة جديدة فيه، أو لأخذ عينة من نقطة واحدة لخطوة معايرة، أو لتصحيح قيمة في موضع معروف. تُتيح وحدة image هذا المستوى من الوصول عبر صيغتي عنونة، تناسب كل منهما طريقة مختلفة في التفكير حول مكان وجود البكسل.
5.4.1. العنونة بالإحداثيات¶
الصيغة الأكثر طبيعية هي تلك التي طوّرت لها صفحة الإحداثيات المفردات بالفعل: تسمية البكسل بإحداثياته الديكارتية (x, y). تأخذ get_pixel() الإحداثيات (x, y) وتُعيد القيمة في ذلك الموضع؛ وتأخذ set_pixel() نفس الإحداثيات (x, y) مع قيمة وتكتبها.
ما تُعيده هذه الاستدعاءات أو تقبله يعتمد على تنسيق الصورة. تحمل الصور بتدرج الرمادي والثنائية وصور Bayer قيمة واحدة لكل بكسل -- سطوعًا في تدرج الرمادي، أو 0 أو 1 في الثنائية، أو عينة قناة لونية واحدة في Bayer -- لذا تُعيد get_pixel() عددًا صحيحًا واحدًا. أما RGB565 فيحمل ثلاث قنوات لونية مُحزّمة في 16 بت، و get_pixel تفكّ تحزيمها إلى صفّ (r, g, b) افتراضيًا، بحيث تُحوّل كل قناة إلى المجال 0 -- 255.
يمكن قلب السلوك الافتراضي عند أي من الطرفين. تمرير rgbtuple=False إلى get_pixel على صورة RGB565 يعود إلى الكلمة الخام المُحزّمة بطول 16 بت -- نفس الصيغة التي يُعيدها الفهرس الخطي، والصيغة الفعّالة عندما يكون التطبيق على وشك كتابة نفس القيمة المُحزّمة مباشرةً. أما تمرير rgbtuple=True على صورة ذات قناة واحدة فيفعل العكس: تُحوَّل القيمة المخزَّنة إلى صفّ RGB888 قبل الإعادة، مع مرور صور Bayer بخطوة إزالة تحزيم Bayer (debayer) فورية. توجد هذه الوسيطة كي يستطيع الكود المُستدعِي طلب البكسلات في فضاء لوني موحَّد بغضّ النظر عن كيفية تخزين الصورة الأساسية لها.
الصور المضغوطة -- JPEG و PNG -- غير مدعومة في get_pixel أو set_pixel. فبايتاتها لا تمثّل بكسلات في مواضع معروفة، وتُطلق الطريقتان خطأً بدلًا من إعادة قيمة لن يكون لها أي معنى.
في الممارسة العملية تبدو الأنماط هكذا:
v = img.get_pixel(40, 30) # grayscale: int 0..255
img.set_pixel(40, 30, 255) # write white
r, g, b = img.get_pixel(40, 30) # RGB565: defaults to (r, g, b) tuple
img.set_pixel(40, 30, (255, 0, 0)) # write red
إذا كانت الإحداثيات (x, y) المطلوبة خارج الصورة، تُعيد get_pixel القيمة None ولا تفعل set_pixel شيئًا. هذا متسامح بحكم التصميم: تسير خوارزميات كثيرة قرب حواف الصورة وتفهرس لحظيًا مواضع خارج المجال، والتجاهل الصامت أقل إزعاجًا من إطلاق استثناء في كل مرة يحدث فيها ذلك.
5.4.2. العنونة بالفهرس الخطي¶
الصيغة الأخرى هي عنونة البكسلات بموضعها في المخزن المؤقت الأساسي. تذكّر تخطيط المخزن المؤقت: تُخزَّن البكسلات صفًّا بعد صفّ، كل بكسلات الصف العلوي أولًا، ثم كل بكسلات الصف التالي، وهكذا نزولًا حتى الأسفل. يعني هذا الترتيب أن لكل بكسل فهرسًا صحيحًا واحدًا يبدأ من 0 عند الزاوية العليا اليسرى ويتزايد على امتداد كل صف بدوره. للبكسل عند الإحداثيات (x, y) فهرس خطي يساوي y * width + x.
تُعنوَن البكسلات بالإحداثيات الديكارتية (x, y) وبفهرس خطي يسير عبر المخزن المؤقت صفًّا بعد صفّ، من اليسار إلى اليمين.¶
تُتيح وحدة image هذا الفهرس عبر تدوين الفهرسة العادي في Python: img[i] تقرأ البكسل عند الفهرس الخطي i، و img[i] = value تكتب واحدًا. ما تُعيده صيغة الفهرس هو القيمة الخام المخزَّنة للتنسيق، وليس الصفّ المفكوك التحزيم الذي تُعيده get_pixel() افتراضيًا. هذا التمييز مهم لأن التنسيق المختار سابقًا يقرّر شكل القيمة الخام:
تعود بكسلات تدرج الرمادي و Bayer كأعداد صحيحة بطول 8 بت.
تعود بكسلات RGB565 و YUV422 كأعداد صحيحة بطول 16 بت -- الكلمة المُحزّمة.
تعود البكسلات الثنائية كـ
0أو1.تعود بكسلات JPEG و PNG كأعداد صحيحة بطول 8 بت، بايتًا واحدًا في كل مرة من التدفق المضغوط. هذه القيم مبهمة -- فهي أجزاء من ترميز مضغوط لا بكسلات بأي معنى عادي.
تناسب صيغة الفهرس الكود الذي يفكّر بالفعل بدلالة إزاحات المخزن المؤقت: حلقة تسير على كل بكسل مرة واحدة، أو خوارزمية تحتاج إلى القفز بمقدار صف في كل مرة، أو قطعة كود تترجم بين تخطيطات المخزن المؤقت. أما الكود الذي يفكّر بدلالة الإحداثيات x و y فتخدمه get_pixel و set_pixel على نحو أفضل؛ فالصيغتان تُعنونان نفس البكسلات عبر نماذج ذهنية مختلفة.
الكائن Image قابل للتكرار أيضًا. تسير for v in img: عبر المخزن المؤقت بنفس ترتيب الصفوف الرئيسي (row-major)، مُنتِجةً القيم الخام بكسلًا واحدًا في كل مرة، و len(img) هي عدد البكسلات للتنسيقات غير المضغوطة أو عدد البايتات للتدفقات المضغوطة.
5.4.3. لماذا تُعدّ معالجة Python على مستوى كل بكسل المسار البطيء¶
ملاحظة عملية تستحق الصراحة بشأنها. السير على الصورة بكسلًا واحدًا في كل مرة من Python بطيء. تحمل صورة بتدرج الرمادي بدقة 320 × 240 عدد 76,800 بكسل؛ واستدعاء get_pixel() على كل منها في حلقة for يُشغّل ملايين تعليمات الشيفرة البايتية (bytecode) في MicroPython لإنجاز عمل يمكن لطريقة أصلية مكافئة إنهاؤه في بضع مئات من الميكروثانية. ليس هذا فرقًا صغيرًا. إنه الفرق بين برنامج نصي يعالج الإطارات في الزمن الحقيقي وآخر يزحف بطيئًا دون معدل إطارات الكاميرا بكثير.
توجد كل طريقة تقريبًا على واجهة Image لأن هناك نسخة أصلية أسرع من نمط شائع على مستوى كل بكسل. تصبح الحلقة التي تجمع صورتين معًا استدعاءً أصليًا واحدًا. وتصبح الحلقة التي تنعّم كل بكسل بحساب متوسطه مع جيرانه استدعاءً آخر. وتصبح الحلقة التي تصنّف كل بكسل مقابل عتبة استدعاءً ثالثًا. ومهمة التطبيق، في معظم الأحيان، هي تمييز أي طريقة على مستوى الصورة الكاملة تطابق العمل الذي كانت الحلقة ستقوم به، واللجوء إليها بدلًا من كتابة الحلقة يدويًا.
تبقى القراءة والكتابة على مستوى البكسل الأداة الصحيحة عندما لا يناسب أي شيء آخر -- ترقيع قياس معيّن في المخزن المؤقت، أو أخذ عينة من موضع واحد لخطوة معايرة، أو تصحيح قيمة في موضع معروف. والمقصود أنها المسار البطيء، يُستخدَم عندما لا تملك طرق الصورة الكاملة الصيغة التي يحتاجها التطبيق، لا بوصفه الطريقة الافتراضية للعمل على البكسلات.