5.1. كائن الصورة Image¶
تتنقل خوارزمية معالجة الصور عبر الصورة بكسلاً واحداً في كل مرة. وعند كل موضع تقوم بشيء بسيط -- قراءة قيمة، ومقارنتها بعتبة، ودمجها مع البكسل المقابل في صورة ثانية، وكتابة نتيجة من جديد. وعند تكرار ذلك عبر الإطار بأكمله، فإن تلك القرارات البسيطة لكل بكسل هي ما يُبنى منه كشف الحواف، وتتبع الكتل، وفك ترميز رموز QR، وكل تقنية أخرى من تقنيات الرؤية الحاسوبية الكلاسيكية. ولأداء هذا العمل بكفاءة، يجب أن تعرف الخوارزمية أين يقع كل بكسل في الذاكرة، وما الذي تعنيه قيمة كل بكسل فعلياً، وأي جزء من الصورة ينبغي أن تنظر إليه. وكائن image.Image هو الكائن الذي ينظم هذه المعلومات.
انتهت مستشعرات الرؤية عند اللحظة التي يعود فيها csi.CSI.snapshot(). أما ما فعلته آلية جانب الكاميرا لإنتاج الإطار الملتقط فقد تم بالفعل؛ والتطبيق الآن يحمل بين يديه كائن Image ويحتاج إلى معرفة ما الذي يفعله به.
5.1.1. المخزن المؤقت وخصائصه¶
يوجد داخل كائن Image مؤشر إلى كتلة متجاورة من البايتات في RAM وترويسة صغيرة تحمل ثلاث معلومات وصفية: عرض الصورة بالبكسل، وارتفاعها بالبكسل، وصيغة البكسل التي تكون البايتات عليها. والبايتات هي البكسلات نفسها، مخزَّنة بترتيب الصفوف أولاً -- جميع بكسلات الصف العلوي أولاً، ثم جميع بكسلات الصف الثاني، وهكذا نزولاً حتى الأسفل. أما الخصائص فتصف كيفية قراءتها.
العرض والارتفاع هما مجرد أعداد صحيحة. أما صيغة البكسل فهي الخاصية الأكثر إثارة للاهتمام، لأنها تحدد عدد البايتات التي يشغلها كل بكسل وما الذي تشفره تلك البايتات. تحمل صورة تدرج الرمادي بايتاً واحداً لكل بكسل يحتوي على قيمة سطوع. وتحمل صورة RGB565 بايتين لكل بكسل يحتويان على حقول الأحمر والأخضر والأزرق مرصوصة في كلمة بطول 16 بت. وتحمل صورة Bayer بايتاً واحداً لكل بكسل، لكن كل بكسل يُؤخذ من خلال أحد ثلاثة مرشحات لونية يُختار وفقاً لموضعه في الفسيفساء. لقد عدّدت مستشعرات الرؤية الكتالوج بأكمله؛ وما يهم هنا هو أن واحدة بالضبط من تلك الصيغ تكون مضبوطة على كل كائن Image، وهذا الاختيار هو ما يحكم حساب عدد البايتات لكل بكسل ومعنى أي بايت مفرد في المخزن المؤقت.
بوجود مؤشر إلى المخزن المؤقت، والعرض، والارتفاع، والصيغة، فإن كل خاصية أخرى قد تريدها الخوارزمية تنتج عن حساب قصير. فالبايت الذي يبدأ به البكسل (x, y) يقع عند الإزاحة (y * width + x) * bytes_per_pixel من بداية المخزن المؤقت. والعدد الإجمالي للبايتات هو width * height * bytes_per_pixel. وعنوان الصف التالي للأسفل يقع بالضبط على بُعد width * bytes_per_pixel بايت بعد بداية الصف الحالي. ويُتيح كائن Image الخصائص الثلاث عبر استدعاءات أساليب بسيطة -- width() و height() و format() -- إضافة إلى size المشتقة عبر size(). وتستخدم الأساليب في مواضع أخرى من الوحدة تلك القيم لإجراء حساب الإزاحة بنفسها؛ ونادراً ما تضطر شيفرة التطبيق لذلك.
إن كائن Image هو غلاف Python صغير يشير إلى كتلة متجاورة من الذاكرة: ترويسة تحمل العرض والارتفاع وصيغة البكسل، يليها مخزن البكسلات نفسه.¶
5.1.2. من أين يأتي المخزن المؤقت¶
القصة الافتراضية في هذا الفصل بأكمله هي تلك التي غطتها مستشعرات الرؤية بالفعل: يصل إطار ملتقط من snapshot، وتكون البايتات موجودة في مخزن إطارات الكاميرا، ويشير كائن Image المُعاد إليها. وتظهر ثلاث طرق أخرى للحصول على واحد بانتظام، وتنطوي كل منها على شيء مختلف بشأن المكان الذي ينتهي إليه المخزن المؤقت.
يبدو التحميل من ملف كأنه تمرير مسار إلى المُنشئ: image.Image("/sdcard/saved.jpg"). تقرأ الوحدة الملف إلى مخزن مؤقت مخصص حديثاً على كومة Python. تُفك شيفرة ملفات BMP و PGM و PPM أثناء الإدخال ويحمل كائن Image الناتج صيغة بكسل غير مضغوطة. أما ملفات JPEG و PNG فتبقى مضغوطة -- يحمل كائن Image الصيغة JPEG أو PNG، ويحتفظ المخزن المؤقت بتدفق بايتات الملف دون تغيير يُذكر. ولإجراء أي عمل على مستوى البكسل على صورة مضغوطة، يحولها التطبيق أولاً عبر to_rgb565() أو to_grayscale()، وهذا التحويل هو حيث يحدث فعلياً فك الضغط -- والتضخم المقابل للكومة، حيث يمكن لصورة JPEG بحجم 30 KB أن تصبح 600 KB من RGB565. والتحميل من ملف هو الأكثر فائدة أثناء التطوير، عندما يحتاج اختبار خوارزمية ما إلى إطار مرجعي معروف مخزَّن إلى جانب البرنامج النصي.
بناء واحدة من الصفر هو حالة اللوحة: يطلب image.Image(320, 240, image.RGB565) من الوحدة تخصيص ذلك العدد من البايتات بتلك الصيغة، وتصفير المحتويات، وإعادة الغلاف. لا تعني البكسلات شيئاً بعد -- فجميعها أصفار -- لكن الصورة الفارغة هي حصان العمل لحفنة من الأنماط المتكررة: الإطارات المرجعية التي يُطرح منها إطار حالي، واللوحات التي تُركَّب عليها الطبقات الفوقية للرسوميات، والمخازن المؤقتة الثنائية التي تُملأ وتُستخدم كأقنعة.
البناء من ndarray يجسر في الاتجاه الآخر، من أي حساب عددي عودة إلى وحدة الصور. يُنتج تمرير ulab.numpy.ndarray من نوع float32 إلى المُنشئ كائن Image تطابق أبعاده الـ ndarray -- يصبح شكل (h, w) ذو المحورين صورة تدرج رمادي، ويصبح شكل (h, w, 3) ذو المحاور الثلاثة RGB565 -- مع قيم الفاصلة العائمة مقيَّسة من 0.0 -- 255.0 إلى نطاق البكسل الصحيح. وتصبح خريطة حرارية لشبكة عصبية، أو مصفوفة عددية من أي نوع، أو أي شيء تنتجه ml أو ulab، شيئاً يمكن لجانب الرسم والفحص في وحدة الصور أن يستخدمه.
تُعيد المصادر الأربعة جميعها النوع نفسه من كائن Image. والشيفرة التي تستخدم الكائن المُعاد لا تضطر أبداً لتتبع مصدره.
5.1.3. عرضان على البايتات¶
في معظم الأوقات تتعامل شيفرة التطبيق مع كائن Image بوصفه كائن صورة مُصنَّفاً -- شيئاً له أساليب مسماة. والنصف الآخر من القصة هو أن الكائن نفسه يظهر أيضاً، بشفافية، كتسلسل مسطح من البايتات لأي واجهة برمجة في MicroPython تأخذ وسيط bytes. والبايتات ليست نسخة من المخزن المؤقت؛ بل هي عرض مباشر له.
هذا الترتيب هو ما يجعل دفع إطار ملتقط خارج الكاميرا أمراً من سطر واحد. فحساب تجزئته (hash)، وإرساله عبر منفذ تسلسلي، وتمريره إلى مقبس شبكة -- لا يحتاج أيٌّ من ذلك إلى خطوة منفصلة "لتحويل الصورة إلى بايتات":
import csi
import hashlib
csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QQVGA)
img = csi0.snapshot()
uart.write(img) # transmits the raw pixel bytes
hashlib.sha256(img) # hashes the same bytes
sock.send(img) # sends them over a socket
العرض الشبيه بالبايتات هو للقراءة فقط افتراضياً، وذلك عمداً. فمخازن الصور كبيرة وأحياناً تكون مشتركة بين طبقات حزمة التصوير، لذا فإن منح عبارة عابرة مثل buf[0] = 0 في مكان عميق من مكدس الاستدعاء القدرة على إفساد أحدها بصمت هو حافة حادة أكثر من أن تُترك مكشوفة. وعندما يكون الوصول إلى البايتات بالقراءة والكتابة هو ما يحتاجه التطبيق فعلاً -- مثل كتابة قيمة معايرة في إزاحة معروفة -- يُعيد bytearray() عرضاً منفصلاً صريح القراءة والكتابة على الذاكرة نفسها، يُشير إلى النية عند موضع الاستدعاء.
5.1.4. أين يقيم المخزن المؤقت¶
مخازن البكسلات كبيرة بما يكفي ليكون مكان وجودها في RAM مهماً. فإطار QQVGA من RGB565 يبلغ 160 × 120 × 2 = 38,400 بايت؛ وإطار VGA من RGB565 يبلغ 614,400 بايت؛ ومدخل RGB565 بقياس 224 × 224 قد يستهلكه مصنِّف شبكة عصبية يبلغ نحو 100 KB. ويمكن أن تكون كومة Python على أصغر الكاميرات بضعة عشرات من الكيلوبايتات فقط بعد إقلاع زمن التشغيل. ومن شأن الاحتفاظ بأكثر من إطار أو إطارين من بيانات الصور على الكومة أن يزاحم كل شيء آخر خارجها.
والمخرج هو أن مخازن الصور في معظمها لا تقيم على كومة Python. بل تقيم في المنطقة المخصصة من RAM التي قدمتها مستشعرات الرؤية باسم مخزن الإطارات -- الذاكرة نفسها التي يكتب فيها DMA الخاص بالكاميرا الإطارات الملتقطة ويقرأ منها معاينة الـ IDE الإطارات المنتهية. ومعظم العمليات على كائن Image تعدل مصدرها في مكانه: تقرأ الخوارزمية البكسلات، وتقرر، وتكتب قيماً جديدة من جديد، ولا تُخصص أي صورة نتيجة منفصلة. أما العمليات التي تُنتج نتيجة منفصلة فعلاً -- تحويلات الصيغ وحفنة من غيرها -- فيمكن أن يُطلب منها وضع تلك النتيجة في مخزن الإطارات عبر الوسيط المفتاحي copy_to_fb. ويفعل copy_to_fb=True شيئين في آن واحد: يضع الصورة الناتجة في مخزن الإطارات بدلاً من الكومة (متجنباً ضغط الكومة) ويجعل النتيجة الإطار التالي الذي ستعرضه معاينة الـ IDE. وإلحاق copy_to_fb=True بالخطوة الأخيرة من سلسلة المعالجة، ومراقبة ظهور النتيجة على الشاشة، والتكرار من هناك هو أحد أكثر أساليب تنقيح الأخطاء فائدة في معالجة الصور.
بوجود غلاف يحمل مخزناً مؤقتاً موسوماً، وأربع طرق لإيجاد واحد، وعرضين على بايتاته، ومفتاح يقرر أين تستقر الجديدة منها، لم يعد كائن Image لغزاً. أما الأسئلة التأسيسية المتبقية -- كيف يُسمى موضع البكسل، وما الذي يحمله كل بكسل فعلاً، وكيف يُحصر نطاق عملية ما على جزء من الصورة -- فهي مبنية فوقه.