12.8. بث الإطارات¶
أكثر الاستخدامات الواقعية شيوعاً للقناة المخصصة هو بث إطارات الصور من الكاميرا إلى برنامج مضيف بمعدل إطارات الكاميرا. الآلية أكثر دقة مما تبدو عليه: قد يصل حجم ملف JPEG إلى 25 كيلوبايت أو أكثر، لذا يقرؤه المضيف على هيئة عدة أجزاء، ويجب منع حلقة الالتقاط في الكاميرا من الكتابة فوق المخزن المؤقت في منتصف القراءة. النمط الصحيح -- المعروض هنا والمستخدم من قبل الأدوات في openmv-projects/tools/ -- يُثبّت المخزن المؤقت حتى ينتهي المضيف من سحب آخر بايت.
12.8.1. جانب الكاميرا¶
قناة إطارات تلتقط داخل مخزن إطارات واحد، وتُثبّته عند أول قراءة من المضيف، ولا تأخذ اللقطة التالية إلا بعد أن يستهلك المضيف الصورة كاملة:
import csi
import protocol
csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QVGA)
csi0.framebuffers(1)
img = csi0.snapshot()
img.compress(quality=85)
img_mv = memoryview(img.bytearray())
img_size = len(img_mv)
frame_available = True
class FrameChannel:
def poll(self):
return frame_available
def size(self):
return img_size
def readp(self, offset, size):
global frame_available
end = offset + size
mv = img_mv[offset:end]
if end == img_size:
# Host has just read the last byte of this frame --
# release the buffer so the capture loop can refresh.
frame_available = False
return mv
ch = protocol.register(name='frame', backend=FrameChannel())
while True:
if not frame_available:
img = csi0.snapshot()
img.compress(quality=85)
img_mv = memoryview(img.bytearray())
img_size = len(img_mv)
frame_available = True
ch.send_event(0x01) # notify host that a new frame is ready
هناك أربعة عناصر تؤدي عملاً حقيقياً هنا:
frame_availableهو المثبِّت. لا تأخذ حلقة الالتقاط لقطة جديدة إلا عندما يكونFalse-- أي أن المضيف قد سحب آخر بايت من الإطار السابق. تعيد قراءة المضيف ضبطه إلىFalseمن داخلreadpبمجرد تقديم الإزاحة النهائية. بدون هذا الحارس، فإنcsi0.snapshot()التالي سيكتب فوق المخزن المؤقت في منتصف القراءة وسيستقبل المضيف إطاراً مُجمّعاً من التقاطتين.ما تنفّذه الواجهة الخلفية هو
readpوليسread. تتعامل مكتبة البروتوكول مع المخزن المؤقت المُعاد باعتباره المرجع وتقرأ بايتاته مباشرة إلى الحزمة الصادرة -- دون نسخ. بالنسبة للحمولات بحجم الإطار يكونreadpأسرع بشكل ملحوظ منread، الذي يفرض نسخة وسيطة.تعيد
sizeطول JPEG المخزّن مؤقتاً دون إعادة حساب أي شيء؛ تحافظ عليه حلقة الالتقاط كلما حدّثت المخزن المؤقت. يستدعي المضيفsizeبينpollوreadpليعرف عدد البايتات التي يجب سحبها.تُخطر
send_event()المضيفَ في اللحظة التي يصل فيها إطار جديد حتى يتمكن من البدء بالسحب دون استطلاع. معرّف الحدث0x01معرّف على مستوى التطبيق ("الإطار جاهز" في هذه الحالة)؛ استخدم عدداً صحيحاً صغيراً مختلفاً لكل نوع من الإخطارات.
12.8.2. التجزئة¶
تُضغط دقة QVGA بتنسيق RGB565 وبجودة JPEG قدرها 85 إلى نحو 10-25 كيلوبايت، وذلك بحسب المشهد -- أكبر بكثير من الحد الأقصى المتفاوض عليه للحمولة على أي كاميرا (انظر الجدول لكل لوحة في protocol.init()). لن تتسع قراءة JPEG واحدة في حزمة واحدة، وهذا لا بأس به، لأن مكتبة البروتوكول تُجزّئها بشفافية.
عندما يطلب المضيف channel_read('frame', 12000):
يُستدعى
readpالخاص بالكاميرا مرة واحدة معoffset=0والطلب الكامل البالغ 12000 بايت. يعيد عرض ذاكرة (memoryview) واحداً يغطي المدى بأكمله.تقسّم مكتبة البروتوكول عرض الذاكرة ذاك إلى أجزاء بحجم الحد الأقصى للحمولة على السلك، حزمة رد
CHANNEL_READواحدة لكل جزء، لكل منها ترويستها وقيمة CRC الخاصة بها. تُبث البايتات من المخزن المؤقت للواجهة الخلفية مباشرة -- دون نسخ.يستقبل المضيف الأجزاء بالترتيب، وتعيد طبقة الموثوقية إرسال أي مقطع يفشل في اجتياز قيمة CRC الخاصة به، وتلصق حزمة تطوير المضيف (SDK) المقاطع معاً في النتيجة البالغة 12000 بايت المُعادة إلى المستدعي.
ملاحظة
هذا هو الفرق العملي الرئيسي بين readp و read. يُستدعى readp مرة واحدة لكل طلب مضيف؛ تُجزّئ طبقة البروتوكول وتُرسل من المخزن المؤقت الواحد المُعاد. يُستدعى read مرة واحدة لكل جزء، وتنسخ المكتبة كل مقطع مُعاد إلى مخزن الحزمة الخاص به. بالنسبة للحمولات بحجم الإطار يوفّر readp كلاً من عبء الاستدعاء على مستوى Python لكل جزء وعبء النسخ.
نصيحة
هل تريد رؤية الفارق بنفسك؟ أعد تسمية دالة readp في الواجهة الخلفية إلى read -- لا شيء آخر يتغير؛ ستلتقط المكتبة قدرة read بدلاً من ذلك -- وقارن عدّاد معدل الإطارات لدى المضيف قبل وبعد. الرقم الأبطأ هو تكلفة النسخ لكل جزء واستدعاء Python التي تتجنبها باستخدام readp.
يحرّر المثبِّت في FrameChannel.readp المخزن المؤقت عندما يكون offset + size == img_size -- اللحظة التي يكون فيها المضيف قد سحب آخر بايت. حتى ذلك الحين، يجب أن يظل المخزن المؤقت صالحاً، ولهذا لا تأخذ حلقة الالتقاط اللقطة التالية إلا بعد أن يعود frame_available إلى False.
12.8.3. جانب المضيف¶
يسحب المضيف الإطارات في حلقة محكمة:
import io
from PIL import Image
from openmv.camera import Camera
with Camera('/dev/ttyACM0', baudrate=921600) as cam:
cam.update_channels()
while True:
size = cam.channel_size('frame')
if not size:
continue
data = cam.channel_read('frame', size)
img = Image.open(io.BytesIO(data))
img.show() # or feed to a GUI
يعمل استدعاء channel_size() أيضاً كفحص لـ"هل هناك شيء جاهز" -- فالصفر يعني أن الكاميرا لم تلتقط بعد -- لذا تتخطى الحلقة محاولات القراءة على مخزن مؤقت فارغ. بالنسبة لتطبيقات الواجهة الرسومية التي تستطلع بالفعل على مؤقت، يكون هذا هو النمط الطبيعي.
تفك Image.open الخاصة بمكتبة Pillow ترميز JPEG؛ فالكاميرا قد ضغطته بصيغة JPEG بالفعل لذا لا يضطر المضيف لإعادة التحزيم البتي المكلف على RGB565. يمكن للبرنامج النصي للمضيف بسهولة مماثلة أن يحفظ البايتات على القرص، أو يسلّمها إلى OpenCV، أو يدفعها عبر عرض ويب.
12.8.4. التفكير في الإنتاجية¶
ثلاثة أشياء تحدّ معدل الإطارات القابل للتحقيق:
معدل التقاط الكاميرا. لا يمكن للبروتوكول تسليم الإطارات أسرع مما ينتجها المستشعر؛ فأي حد يفرضه تنسيق البكسل المختار وحجم الإطار على الالتقاط هو السقف.
الحد الأقصى المتفاوض عليه للحمولة. تعني الحمولات الأكبر عدداً أقل من الأجزاء لكل إطار وعبء تأطير أقل، لذا تنقل الكاميرات ذات مخازن البروتوكول الأكبر البايتات أسرع من الأصغر.
عبء CRC و ACK. تكلّف كل حزمة 14 بايت من التأطير بالإضافة إلى جولة ذهاب وإياب واحدة لـ ACK. بالنسبة للأجزاء الطويلة يكون العبء لكل حمولة صغيراً؛ أما بالنسبة للحمولات الصغيرة فإنه يهيمن.
بالنسبة لمعظم أعمال الواجهة الرسومية من الكاميرا إلى الحاسوب المحمول، يكون العامل المحدِّد هو زمن الالتقاط وضغط JPEG في الكاميرا، وليس مكدّس البروتوكول. وحيث يصبح البروتوكول فعلاً عنق الزجاجة -- عند بث إطارات خام غير مضغوطة بمعدلات إطارات عالية مثلاً -- تكون الأدوات المتاحة هي إيقاف ACKs (protocol.init(ack=False))، أو زيادة مخزن البروتوكول إن كانت الكاميرا تدعم ذلك، أو الالتقاط بتدرج الرمادي GRAYSCALE بحيث يحمل كل ملف JPEG مضغوط قناة واحدة بدلاً من ثلاث وينتهي الأمر بالإطار المُرمَّز أصغر بشكل ملحوظ على السلك.
قناة الإطارات هي تدفق البيانات النموذجي من الكاميرا إلى المضيف. نفس واجهة الواجهة الخلفية، مع إضافة دالة write، تتيح للمضيف دفع البيانات في الاتجاه الآخر أيضاً -- وهو ما تحتاجه أداة الكاميرا التفاعلية بمجرد أن يرغب المشغّل في تغيير شيء ما بدلاً من مجرد المشاهدة.