5.3. تنسيقات البكسل

الخوارزمية التي تكشف الحواف تتوقع أن يحمل كل بكسل قيمة سطوع. والخوارزمية التي تتعقب جسماً ملوناً تتوقع أن يحمل كل بكسل لوناً. والخوارزمية التي تنفّذ الإغلاق المورفولوجي تتوقع أن يكون كل بكسل إما مفعّلاً أو معطّلاً. إن تنسيق البكسل الذي تحمله Image -- وهو أحد التنسيقات المعدّدة في كتالوج مستشعرات الرؤية -- هو ما يجعل تلك التوقعات قابلة للتحقق منها مسبقاً: فالتنسيق يحدد سلفاً الشكل الذي توجد عليه البكسلات، وأي الخوارزميات يمكنها بالتالي أن تعمل عليها دون خطوة تحويل.

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

A vertical stack of five labelled byte-layout strips. BINARY shows one byte split into eight single-bit cells, marked "8 pixels per byte". GRAYSCALE shows three labelled single-byte cells each marked "1 pixel". RGB565 shows two adjacent bytes with bit fields RRRRR GGGGGG BBBBB labelled "1 pixel". YUV422 shows four labelled byte cells Y0, U, Y1, V marked "2 pixels". BAYER shows two rows of four labelled byte cells: R G R G on the top row, G B G B on the bottom row.

تنسيقات البكسل غير المضغوطة الخمسة وكيفية تجميع بايتاتها. لم يُرسَم JPEG وPNG هنا لأنهما تيارات مضغوطة متغيرة الطول وليسا شبكات بكسل ثابتة الحجم.

5.3.1. حصان العمل في تدرج الرمادي

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

يقدّم تنسيق تدرج الرمادي للخوارزميات تلك القيمة بالضبط دون أي عبء إضافي. فبايت واحد لكل بكسل يحمل قيمة سطوع من 0 (أسود) حتى 255 (أبيض). حجم هذا التنسيق هو نصف حجم RGB565 وYUV422 وثُلث حجم RGB888، لذا فكل عملية تجري على بيانات أقل -- بشكل أسرع وبضغط أقل على المخزن المؤقت. وعلى الكاميرات الأصغر، حيث يتنافس مخزن الإطارات مع بقية البرنامج النصي على RAM، قد يكون فارق البصمة هذا هو ما يحدد ما إذا كان المسار يتسع أصلاً. وإذا لم يكن اللون هو الدليل الذي تحتاجه الخوارزمية، فإن تدرج الرمادي هو الإجابة الصحيحة.

5.3.2. اللون عبر RGB565

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

إن عرض إطار مُعلَّق عليه -- رسم مربعات الكشف، وكتابة نص تشخيصي، وإيصال الإطار إلى شاشة أو إلى عارض بعيد -- يستدعي بطبيعته أيضاً RGB565. فمعاينة IDE، ووحدات التحكم في الشاشة على اللوحة، ومعظم وجهات الشبكة إما تستهلك التنسيق مباشرةً أو تحوّل منه بتكلفة زهيدة.

5.3.3. Bayer بوصفه تنسيق التخزين

صورة Bayer هي خرج المستشعر الخام، قبل أن يقوم ISP بفك تشفير Bayer منها إلى تمثيل لوني نهائي. كل بكسل هو بايت واحد يحمل قناة لون واحدة -- وهي القناة التي مرّرها مرشّح اللون في ذلك الموضع من الفسيفساء. وهذا يجعل صورة Bayer بنفس حجم صورة تدرج الرمادي وثُلث حجم RGB888، وهو ما يتماشى مع ما يكون Bayer مفيداً له فعلاً: تخزين إطارات كثيرة في آنٍ واحد عندما تكون RAM هي القيد الملزم.

العقبة هي أن الخوارزميات في وحدة image لا تعمل على صور Bayer مباشرةً. فبدون فك تشفير Bayer، لا يحمل أي بكسل معلومات كافية لاتخاذ حكم لوني بمفرده، والأنماط التي تبحث عنها الخوارزميات -- الحواف والزوايا والكتل -- ستُشوَّه بفعل الفسيفساء. والطريقتان الوحيدتان لقراءة صورة Bayer أو تعديلها هما get_pixel() وset_pixel()؛ وكل ما عداهما يتوقع تمثيلاً نهائياً.

النمط الذي ينتج عن ذلك هو تخزين الإطارات بتنسيق Bayer طالما احتاجت إلى البقاء في طابور، وتحويل كل واحد منها إلى تدرج الرمادي أو RGB565 في اللحظة التي تبدأ فيها معالجته فعلاً. يكلّف التحويل دورات معالج لكنه يوفّر RAM التي كانت ستُحجَز لولا ذلك للاحتفاظ بالإطارات النهائية طوال عمر التطبيق.

ملاحظة

العمليات الوحيدة لوحدة image على بكسلات Bayer مباشرةً هي get_pixel() وset_pixel()، ومسار ترميز JPEG الذي يغذّي معاينة IDE أو عارضاً بعيداً. أما الرسم والتحليل والترشيح فكلها تتطلب التحويل أولاً إلى تدرج الرمادي أو RGB565 أو ثنائي.

5.3.4. YUV422 للمسارات التي تريد كليهما

يفصل YUV422 معلومات كل بكسل إلى قناة لمعان (Y) وقناتي لونية (U وV)، ويُجري عيّنة فرعية للّونية بحيث يتشارك زوجا البكسل المتجاوران قيمة U واحدة وقيمة V واحدة. ويبلغ متوسط البايتات لكل بكسل اثنين -- وهو نفس RGB565 -- لكنها مرتّبة بحيث تكون قناة Y بالفعل صورة تدرج رمادي مستمرة بعمق 8 بِت موجودة عند إزاحات معروفة في المخزن المؤقت.

هذا التخطيط هو بالضبط ما يريده المسار عندما تكون بعض مراحله عملاً بتدرج الرمادي وبعضها يحتاج إلى لون. فقراءة قيم Y مباشرةً لمراحل تدرج الرمادي تتخطى تكلفة تحويل صريح؛ وتكون قناتا U وV متوفرتين عندما تحتاج مرحلة لاحقة إلى اللون فعلاً. وخارج هذا النمط المحدد، يكون RGB565 عادةً الخيار الأبسط للون ويكون تدرج الرمادي الخيار الأبسط للعمل المعتمد على السطوع فقط -- وتأتي قيمة YUV422 من كونه جيداً في كليهما في الوقت ذاته.

ملاحظة

تعمل وحدة image على YUV422 بطريقة أكثر محدودية مما تعمل به على تدرج الرمادي أو RGB565 أو الثنائي -- قراءات مباشرة لقناة Y للعمل بتدرج الرمادي، ومسار ترميز JPEG الذي يغذّي معاينة IDE أو عارضاً بعيداً. أما الطرق المدركة للون فتتوقع RGB565؛ وتحتاج إطارات YUV422 إلى تحويل صريح قبل تحليل اللون أو الرسم.

5.3.5. الثنائي، الأقنعة، والخرج المعتمد على العتبة

الصورة الثنائية هي بِت واحد لكل بكسل: كل بكسل هو إما 0 أو 1. نادراً ما يظهر هذا التنسيق بوصفه التقاطاً من المستشعر؛ بل يظهر كخرج طبيعي للعتبة (حيث يصنّف اختبار لون أو سطوع كل بكسل إلى "نعم، يتطابق" أو "لا، لا يتطابق") وكمدخل طبيعي للعمليات المورفولوجية ولوسيطة mask التي تقبلها طرق كثيرة.

الميزة العملية لهذا التنسيق هي حجمه. فالصورة الثنائية تبلغ ثُمن بصمة صورة تدرج الرمادي، لذا فحمل قناع كبير -- خيار لكل بكسل بشأن أي المواضع ينبغي لعملية لاحقة أن تمسّها -- يكون زهيداً. وكون عمليات كثيرة تقبل صورة ثنائية بوصفها وسيطة كلمة مفتاحية mask= هو الوجه الآخر للنقطة نفسها: التنسيق صغير، وربط الخرج الثنائي لمرحلة ما بمدخل القناع لمرحلة أخرى نمط مسار شائع.

5.3.6. JPEG وPNG عند الحدود

كائنات Image من نوع JPEG وPNG مختلفة عن غيرها في الكتالوج. فهي ليست شبكات بكسل؛ بل تيارات بايتات مضغوطة تُرمّز بيانات البكسل في شكل لا تستطيع العمليات على مستوى البكسل قراءته. واستدعاء get_pixel() على JPEG لا يُرجع البكسل عند موضع ما؛ فالبكسل غير موجود مفكوكاً في أي مكان في المخزن المؤقت لكي تجلبه الطريقة.

يظهر JPEG وPNG عند حدود معالجة الصور، حيث تغادر بيانات البكسل الكاميرا أو تدخلها في شكل مضغوط. فحفظ إطار على القرص بصيغة JPEG يبقي الملف صغيراً؛ وإرسال إطار عبر شبكة بصيغة JPEG يبقي الإرسال زهيداً؛ وتحميل إطار مرجعي من ملف JPEG يتيح له البقاء على القرص في شكل أصغر بكثير مما ستكون عليه البكسلات الخام. ولأي من حالات الاستخدام تلك يكون التمثيل المضغوط هو الإجابة الصحيحة. لكن لإجراء أي معالجة فعلية على JPEG، يحوّله التطبيق أولاً إلى تنسيق قابل للعمل عليه -- وذلك التحويل هو حيث تتمدد البايتات المضغوطة إلى بكسلات وحيث يحدث فعلاً تضخّم المخزن المؤقت (إذ يمكن لملف JPEG بحجم 30 KB أن يصبح 600 KB من RGB565).

5.3.7. التحويل بين التنسيقات

مسار التحويل هو ما يخيط التنسيقات المختلفة في مسار واحد. خمس طرق في الفئة Image تأخذ صورة موجودة وتُرجع صورة جديدة بتنسيق مختلف:

  • to_grayscale() تنتج صورة بايت واحد لكل بكسل، وهو التنسيق الذي تريده الخوارزميات الكلاسيكية.

  • to_rgb565() تنتج تنسيق اللون ثنائي البايت لكل بكسل الذي تتحدث به كلٌّ من الطرق المدركة للون ومعاينة IDE.

  • to_bitmap() تنتج صورة ثنائية بِت واحد، وهو التنسيق الذي تقبله المورفولوجيا ووسيطات mask.

  • to_jpeg() تنتج صورة مضغوطة بصيغة JPEG مناسبة للحفظ أو الإرسال.

  • to_png() تنتج صورة مضغوطة بصيغة PNG عندما يُفضَّل الترميز عديم الفقد على ملفات JPEG الأصغر.

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

عندما يكون المصدر ما زال مطلوباً -- عندما يتعيّن على مرحلة لاحقة من المسار أن ترى الإطار الأصلي -- تتجاوز وسيطتا كلمة مفتاحية السلوك الافتراضي في المكان. فـ copy=True تخصص مخزناً مؤقتاً منفصلاً للصورة المحوَّلة على كومة Python وتترك المصدر سليماً. أما copy_to_fb=True فتجري التخصيص نفسه لكنها تضعه في مخزن الإطارات بدلاً من الكومة -- وهو ما يلجأ إليه التطبيق عندما يحتاج أن تستقر الصورة المحوَّلة في معاينة IDE، لأن IDE تقرأ من مخزن الإطارات.

تنتج طريقتان إضافيتان صور RGB565 ملوّنة عبر لوحة ألوان بدلاً من تحويل مباشر. تربط to_rainbow() كل قيمة إدخال أحادية القناة بلون على امتداد تدرّج سلس يمر عبر الطيف المرئي. وتربط to_ironbow() كل قيمة إدخال بلوحة المصوِّر الحراري غير الخطية التي تمتد من الأسود مروراً بالأحمر الداكن والبرتقالي وصولاً إلى الأبيض. وكلتاهما أداتا تصوّر مرئي وليستا أداتي قياس؛ والهدف هو جعل صورة أحادية القناة، تكون قيمها الخام لولا ذلك غير مرئية للعين، قابلة للقراءة بنظرة واحدة.

5.3.8. حجم المخزن المؤقت

تفصيل أخير حول التنسيقات يستحق التوضيح. تُبلّغ size() دائماً عن حجم المخزن المؤقت بالبايت، وليس عن عدد البكسلات. فبالنسبة للتنسيقات غير المضغوطة ينتج ذلك مباشرةً عن الأبعاد وعدد البايتات لكل بكسل: width * height * bytes_per_pixel. أما بالنسبة لـ JPEG وPNG فهو حجم التيار المضغوط، الذي يتغير من إطار لآخر تبعاً لما يحتويه المشهد. والكود الذي يخصص مخازن مؤقتة من ميزانيات بايت يستخدم size() للحالة الأولى؛ والكود الذي يدفق إطارات مضغوطة من الكاميرا يقرؤه بعد كل عملية ضغط لمعرفة عدد البايتات التي يحتويها التيار فعلاً.