5.33. تدفقات ImageIO

تغطي save() و to_jpeg() حالة الإدخال/الإخراج أحادية الإطار: يلتقط التطبيق إطاراً، ويرمّزه، ثم يدفعه إلى مكان ما. وهناك صنف مختلف من التطبيقات يحتاج إلى حالة التسلسل: تسجيل عدة إطارات متتالية بمعدل الالتقاط الطبيعي، وتخزينها في مكان يمكن استرجاعها منه لاحقاً، وإعادة تشغيلها بالسرعة الصحيحة. فبرنامج نصي لجمع بيانات التدريب يلتقط بضع مئات من الإطارات النموذجية لخط معالجة تعلّم آلي؛ ومحطة تفتيش تسجّل كل قطعة ملتقطة لأغراض التتبّع؛ وبرنامج نصي تطويري يعيد تشغيل تسلسل مخزّن لاختبار خوارزمية جديدة على بيانات سبق التقاطها مباشرة.

صنف ImageIO هو مسجّل/مشغّل وحدة الصورة. يحتوي التدفق الواحد على تسلسل من إطارات Image -- ربما بأحجام وصيغ بكسل مختلفة -- مع الفاصل الزمني بين الإطارات لكل منها، بحيث يمكن لإعادة التشغيل أن تعيد إنشاء معدل الإطارات الأصلي. ويتوفر مخزنا تخزين خلفيان: ملف على نظام الملفات أو مخزن مؤقت ثابت الحجم في RAM.

5.33.1. مخزنا التخزين الخلفيان

يحفظ تدفق الملف التسجيل عبر دورات الطاقة، ويتحدد حجمه فقط بالتخزين الداعم له. يبدأ برأس سحري بطول 16 بايت OMV IMG STR Vx.y متبوعاً بقطعة واحدة لكل إطار؛ ويُصدر الكاتب الحالي V2.0 بينما لا يزال القارئ يقبل ملفات V1.0 و V1.1 لأغراض التوافق مع الإصدارات السابقة. ومسار الملف هو وسيطة المُنشئ؛ والوضع هو وضع فتح الملف ('r' لقراءة تدفق موجود، 'w' للاقتطاع والكتابة من جديد).

# Recording to /sdcard/run.bin
stream = image.ImageIO("/sdcard/run.bin", "w")
for _ in range(120):
    img = csi0.snapshot()
    stream.write(img)
stream.close()

يقيم تدفق الذاكرة في مخزن مؤقت من RAM يُخصَّص عند الإنشاء. يأخذ المُنشئ صفاً ثلاثياً (w, h, pixformat) بدلاً من مسار، وتصبح وسيطة mode هي عدد فتحات الإطارات المخصَّصة مسبقاً. ويُحدَّد حجم المخزن المؤقت بدقة لذلك العدد من الإطارات بالأبعاد المُعطاة، ولا يُسمح له بالنمو بمجرد تخصيصه -- فالكتابة بعد الفتحة الأخيرة تثير EOFError، وكتابة إطار أكبر من مخزن الفتحة المؤقت تثير ValueError. وتُعدّ تدفقات الذاكرة الأداة المناسبة عندما يحتاج التطبيق إلى تسليم تسجيل إلى مرحلة لاحقة دون المرور عبر نظام الملفات (مخزن حلقي قصير من الإطارات الأخيرة لنمط التشغيل بعد الإطلاق، على سبيل المثال).

# Pre-allocate space for 32 QVGA RGB565 frames in RAM
stream = image.ImageIO((320, 240, image.RGB565), 32)
for _ in range(32):
    stream.write(csi0.snapshot())

بالنسبة لصيغ البكسل المضغوطة (image.JPEG، image.PNG) يُقدَّر حجم الفتحة بمعدل 2 بت لكل بكسل؛ والإطار المرمَّز الأكبر من التقدير يثير ValueError وقت الكتابة، لذا يتعين على التطبيق الذي يتوقع تخزين ملفات JPEG عالية الجودة إما الإفراط في تخصيص عدد الفتحات أو الترميز بجودة أقل أولاً.

تُعيد type() القيمة image.ImageIO.FILE_STREAM أو image.ImageIO.MEMORY_STREAM بحيث يمكن للكود اللاحق التكيّف مع أي مخزن تخزين خلفي يُعطى له.

5.33.2. التسجيل

تُلحِق write() إطار Image ملتقطاً بتدفق ملف (أو تخزّنه في الفتحة الحالية لتدفق ذاكرة) وتقدّم الإزاحة بمقدار واحد. ويسجّل الاستدعاء نفسه الفاصل الزمني بين الإطارات منذ آخر كتابة، بحيث يمكن لشقّ إعادة التشغيل التوقف للمدة الصحيحة بين الإطارات، ويُحافَظ على معدل الإطارات الطبيعي للتسجيل.

يُسمح بالإطارات غير المتجانسة ضمن تدفق ملف واحد: يمكن لتسجيل أن يمزج بحرية بين لقطات RGB565 وقصاصات بتدرج الرمادي وصور مصغّرة مرمَّزة بصيغة JPEG، وسيفكّ القارئ ترميز كل منها بحجمه وصيغته الأصليين. أما تدفقات الذاكرة فمتجانسة (تتشارك جميع الفتحات في القيم (w, h, pixformat) التي زوّد بها المُنشئ)، لذا يقتصر تسجيل الذاكرة على تكوين إطار واحد.

تُعيد write() كائن التدفق بحيث يمكن تسلسل الاستدعاءات. والكتابة عند إزاحة ليست في النهاية لتدفق ملف تقتطع بقية الملف -- وهو أمر مفيد لتحرير تسلسل مخزّن، لكنه محفوف بالمخاطر إذا نُقل موضع الكتابة التالية عن غير قصد عبر seek() سابقة.

تُفرّغ sync() الكتابات المعلّقة إلى القرص لتدفقات الملفات (وهي عملية لا تفعل شيئاً على تدفقات الذاكرة)، وينبغي استدعاؤها دورياً عندما يكون التسجيل طويل الأمد، لتجنّب فقدان نهاية التسجيل إذا أُعيد إقلاع الكاميرا قبل إغلاق الملف. ويغلق المُدمِّر التدفق تلقائياً عندما يخرج ImageIO عن النطاق، لكن استدعاء close() الصريح هو الانضباط الصحيح.

5.33.3. إعادة التشغيل

تقرأ read() الإطار عند الإزاحة الحالية، وتقدّم الإزاحة، وتُعيد Image الجديد. ويبقى المُستقبِل في مخزن الإطارات عندما يكون copy_to_fb=True (الافتراضي) بحيث تكون الصورة المُعادة قابلة للرسم عبر معاينة الـIDE؛ ومع copy_to_fb=False يحلّ الإطار على كومة MicroPython.

# Loop a recorded stream at its natural frame rate
stream = image.ImageIO("/sdcard/run.bin", "r")
while True:
    img = stream.read()
    # img is now in the frame buffer; the IDE shows it
    # and the script can run any analysis it likes

تتحكم كلمتان مفتاحيتان في سلوك إعادة التشغيل. loop=True (الافتراضي لتدفقات الملفات) يُرجِع مؤشر القراءة إلى البداية عند بلوغ نهاية التسجيل، بحيث لا يُعيد الاستدعاء أبداً None؛ و loop=False يُعيد None بمجرد استنفاد التسجيل وينتهي حلقة المستدعِي. أما pause=True (الافتراضي) فيحجب الاستدعاء حتى ينقضي الفاصل الزمني بين الإطارات المسجَّل وقت الكتابة، بحيث يطابق معدل إطارات إعادة التشغيل معدل إطارات الالتقاط الأصلي؛ و pause=False يُعيد فوراً، وهو مفيد لخطوط معالجة التحليل التي تريد المرور على التسجيل بأسرع ما يمكن دون احترام التوقيت الأصلي.

يعمل نمط الحلقة نفسه مع تدفقات الذاكرة باستثناء أن loop يُتجاهَل -- فالقراءة بعد نهاية تدفق الذاكرة تثير EOFError. والنمط المتوقع لحلقة الذاكرة هو استخدام seek() للرجوع إلى الصفر صراحة عند الرغبة في الالتفاف.

5.33.5. تسجيلات قابلة للتشغيل على المضيف

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

تغطي وحدتان منفصلتان حالة القابلية للتشغيل على المضيف. فوحدة mjpeg تسجّل Motion JPEG: تسلسلاً من الإطارات المضغوطة بصيغة JPEG ومحزومة في حاوية واحدة بأسلوب AVI يشغّلها مباشرة VLC و QuickTime و ffmpeg ووسم فيديو الويب القياسي جميعها. ووحدة gif تسجّل صورة GIF متحركة: تسلسلاً من الإطارات غير المضغوطة (أو المضغوطة باللوحة اللونية) مع تأخيرات صريحة لكل إطار، قابلاً للتشغيل في أي متصفح ويب أو عارض صور يتعامل مع صور GIF المتحركة.

وحدة mjpeg هي الخيار الطبيعي للتسجيلات الطويلة. فضغط JPEG يبقي حجم الملف قابلاً للإدارة -- مماثلاً لـ to_jpeg() بالجودة المُعدّة، إطاراً بعد إطار -- بحيث تبقى جلسة الالتقاط الممتدة ضمن حدود سعة بطاقة SD. والاستخدام يعكس عن قرب تسجيل ImageIO:

import mjpeg

m = mjpeg.Mjpeg("/sdcard/run.mjpeg")
while running:
    m.add_frame(csi0.snapshot(), quality=85)
m.close()

يقبل mjpeg.Mjpeg نفس الكلمات المفتاحية الموضعية وكلمات القياس بأسلوب الرسم التي تأخذها طرائق الصورة الأخرى، بحيث يمكن قياس التسجيل أو قصّه أو تعيينه باللوحة اللونية لكل إطار أثناء الإدخال. وتعود وسيطتا width و height للمُنشئ افتراضياً إلى أبعاد مخزن الإطارات الرئيسي وتثبّتان دقة الإخراج؛ ويُقاس كل إطار مُلحَق (مع الحفاظ على نسبة العرض إلى الارتفاع) ليتسع. وتُفرّغ sync() الملف إلى القرص أثناء تسجيل طويل، وتُنهي close() الحاوية -- فملف Motion JPEG الذي لم يُغلَق بشكل نظيف غير قابل للتشغيل، لذا فإن الانضباط مهم.

وحدة gif هي الخيار الطبيعي للتسجيلات القصيرة التي تُشارَك حرفياً مع مشاهد غير تقني -- بضع ثوانٍ من الحركة ملتقطة لعرض توضيحي، أو رسم توضيحي متحرك للوثائق، أو مقطع حدث مُضمَّن في رسالة دردشة. تُخزَّن إطارات GIF غير مضغوطة (أو مضغوطة باللوحة اللونية بعمق لوني 7 بت)، مما يجعل الملفات أكبر بكثير لكل ثانية من Motion JPEG ويستبعد الصيغة للتسجيلات الأطول من بضع ثوانٍ، لكن النتيجة تُسقَط مباشرة في أي متصفح:

import gif

g = gif.Gif("/sdcard/clip.gif")
while running:
    g.add_frame(csi0.snapshot(), delay=10)
g.close()

وسيطة delay في add_frame() هي زمن عرض كل إطار بالسنتي ثانية (10 يعني 100 مللي ثانية لكل إطار، أو 10 إطار في الثانية)، وهي عنصر التحكم القياسي في تشغيل GIF. وتحدد الكلمة المفتاحية loop للمُنشئ ما إذا كان المقطع الناتج يُكرَّر تلقائياً في العارضات (الافتراضي هو True، وهو ما يطابق التوقع التقليدي لـ"صورة GIF متحركة").

تغطي مسارات التسجيل الثلاثة فيما بينها الحالات الشائعة: ImageIO لإعادة المعالجة على الكاميرا، و Motion JPEG للتسجيلات الطويلة القابلة للتشغيل على المضيف، وصورة GIF المتحركة للمقاطع القصيرة القابلة للتشغيل على المضيف. ويتلخص الاختيار بينها في من يعيد تشغيل التسجيل. فالمرحلة اللاحقة التي تعمل على الكاميرا نفسها تقرأ ImageIO؛ ومحطة عمل مضيفة أو عارض ويب يقرأ MJPEG أو GIF.

5.33.6. نمط التشغيل بعد الإطلاق

يجمع نمط مفيد بين تدفق ذاكرة وشرط إطلاق. تسجّل الكاميرا باستمرار في مخزن حلقي للذاكرة بعدد count من الفتحات، فتكتب فوق الفتحة الأقدم في كل دورة. وعندما يُطلَق شرط الإطلاق (دخول كتلة إلى الإطار، أو تجاوز حدث حركة للعتبة، أو الضغط على زر) يلتقط التطبيق محتويات الحلقة -- أحدث count من الإطارات -- ويكتبها في تدفق ملف على بطاقة SD. والنتيجة هي تسجيل ما قبل الإطلاق الذي يلتقط الثواني السابقة للحدث الذي لاحظته الكاميرا فعلاً، لا الثواني التالية له فحسب، وهو القيد الكلاسيكي لمسجّل "الالتقاط عند الإطلاق" الساذج.

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