5.11. مقارنة الإطارات (Frame differencing)

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

5.11.1. خط المعالجة الأساسي

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

reference = csi0.snapshot().copy()

.copy() مهمّة. فاستدعاء csi0.snapshot() بمفرده يُرجع كائن Image يوجد مخزنه في مخزن الإطارات، حيث سيكتب فوقه الاستدعاء التالي لـ snapshot. أما .copy() فتخصّص مخزنًا منفصلًا للمرجع وتتيح لبكسلات هذا الإطار البقاء بعد اللقطة التالية.

تُنفَّذ المرحلة الثانية على كل إطار: التقاط صورة جديدة، ثم حساب الفرق المطلق بينها وبين المرجع. وهذا بالضبط ما تفعله difference():

current = csi0.snapshot()
current.difference(reference)

بعد هذا الاستدعاء، تحمل current صورة تشير بكسلاتها غير الصفرية إلى كل موضع تغيّر فيه المشهد منذ التُقط المرجع، مع تناسب مقدار كل بكسل مع حجم التغيّر في ذلك الموضع.

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

تستخرج المرحلة الرابعة المناطق المتصلة من ذلك القناع الثنائي -- مجموعات من البكسلات غير الصفرية المتجاورة التي تشكّل رقعًا متلاصقة. تفعل find_blobs() ذلك في استدعاء واحد، مُرجِعةً قائمة بمناطق الحركة، لكل منها مربع إحاطة وعدد بكسلات، يمكن لبقية التطبيق التصرف بناءً عليها.

A horizontal pipeline diagram. The leftmost two panels are a reference frame and a current frame side by side, with a plus mark between them. An arrow leads from the pair to a third panel labelled difference, in which a few patches are bright against a dark background. An arrow leads from there to a fourth panel showing a binary thresholded version of the difference, with the same patches now solid white. A final arrow leads to a fifth panel showing the binary mask annotated with rectangular bounding boxes drawn around each patch.

خط معالجة مقارنة الإطارات: إطار مرجعي إضافةً إلى إطار حالي يصيران صورة فرق؛ ثم يحوّل تطبيق العتبة الفرق إلى قناع ثنائي للمواضع المتغيّرة؛ وتحوّل خطوة المناطق المتصلة القناع إلى قائمة بمناطق الحركة.

5.11.2. المراجع في الذاكرة وعلى القرص

يحتفظ خط المعالجة الأساسي بالإطار المرجعي في RAM. وهذا هو الخيار الصحيح حين يُلتقط المرجع في هذا التشغيل للبرنامج النصي وعليه فقط أن يبقى ما دام البرنامج النصي قيد التشغيل.

أما بالنسبة لتطبيق طويل الأمد -- كاميرا ينبغي أن تستأنف كشف التغيّر بعد دورة طاقة، أو برنامج نصي متقطّع يحتاج إلى كشف أي تغيّر منذ لحظة سابقة ما -- فيجب أن يبقى الإطار المرجعي بعد توقّف البرنامج النصي قيد التشغيل. والنمط هو حفظ المرجع على القرص:

csi0.snapshot().save("/sdcard/reference.bmp")

وتحميله من جديد في بداية كل تشغيل:

reference = image.Image("/sdcard/reference.bmp")

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

5.11.3. عزل مصدر الضوء

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

5.11.4. اختيار difference أو sub

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

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