6.1. لماذا المصفوفات

صنف Image هو الأداة المناسبة للعمل على البكسلات لأن كل طريقة فيه تعمل مباشرة على مخزن البكسل الأصلي للكاميرا في استدعاء واحد سريع. ومعظم ما يفعله التطبيق بالإطار -- العتبة، وإيجاد الكتل، وكشف AprilTag، ومرشحات الحواف -- موجود هناك بالفعل.

ما لا تعرضه مكتبة الصور هو بقية العمل العددي الذي يصادفه تطبيق OpenMV:

  • مخازن المستشعر التي ليست بكسلات -- عينات ADC، ومحاور من IMU (وحدة قياس بالقصور الذاتي)، وصوت الميكروفون،

  • أرقام مشتقة من الصورة لا تعيدها أي طريقة مدمجة -- عمود مدرج تكراري، أو مزيج مخصص من إطارين، أو تحويل لكل بكسل لا يغطيه الفهرس،

  • جبر خطّي صغير -- مصفوفة المعايرة التي تصحّح العدسة، والدوران الذي يدمج بيانات IMU،

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

كل هذه تتطلب نفس الشكل: مخزن من الأرقام بعملية واحدة مطبقة على كل عنصر. وحلقة for في Python هي الطريقة البديهية لكتابتها:

for i in range(len(samples)):
    samples[i] = samples[i] * cal

تعمل الحلقة. لكنها أيضًا بطيئة. فـ Python لغة مُفسَّرة، وكل تكرار في حلقة Python يحمل تكلفة تشغيل المفسّر مرة واحدة: البحث عن samples، وقراءة العنصر i، والضرب، والكتابة مجددًا، وزيادة عدّاد الحلقة، والتحقق من شرط الحلقة. وعلى مخزن من ألف عينة مستشعر، تتراكم تكاليف المفسّر هذه إلى عشرات الأجزاء من الألف من الثانية لما هو في جوهره عملية سريعة.

يلدغ هذا العبء في كل مرة يصل فيها برنامج نصي إلى مخزن. فإطار QVGA بتدرج الرمادي يبلغ 76,800 بكسل؛ ومقياس تسارع بتردد 100 هرتز يقدّم مئة عينة ثلاثية المحاور في الثانية؛ وميكروفون يملأ مخزنًا بـ 1024 عينة كل 64 مللي ثانية. وحلقة for خالصة من Python على أي من هذه تحوّل مهمة ينبغي أن تستغرق بضعة ميكروثوانٍ إلى مهمة تستغرق عشرات الأجزاء من الألف من الثانية -- وأطول بنحو عشرة أضعاف مرة أخرى على مخزن بحجم صورة.

6.1.1. دوال المكتبة أسرع من الحلقات

الحل هو التعبير عن العملية كاستدعاء دالة واحد على المخزن بأكمله، بدلًا من حلقة Python على عناصره. وnumpy هو بالضبط ذلك: مكتبة لحساب المصفوفات حيث كل عملية هي دالة واحدة مُحسَّنة مسبقًا تسير على المخزن مرة واحدة من البداية إلى النهاية. فـ np.multiply(samples, cal) تضرب كل عنصر من samples في cal داخل استدعاء واحد -- نفس الحساب الذي قامت به الحلقة، دون تكلفة المفسّر لكل تكرار. ونفس عملية الضرب لـ 1000 عنصر التي استغرقت عشرات الأجزاء من الألف من الثانية كحلقة Python تستغرق عشرات الميكروثواني كاستدعاء numpy.

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

6.1.2. لماذا لا تفي القائمة بالغرض

لا يمكن لـ list في Python أن تحلّ محلها. فالقائمة يمكن أن تحمل أي خليط من الكائنات -- أعداد صحيحة، وأعداد عشرية، وسلاسل نصية، وقوائم أخرى -- ودالة المكتبة التي تقرؤها لا تزال مضطرة للنظر في كل خانة لمعرفة ما بداخلها واستخراج القيمة قبل أن يحدث أي حساب. وهذا العبء لكل خانة هو بالضبط التكلفة التي تدفعها حلقة Python. فالقوائم غير مناسبة لحساب المصفوفات السريع.

6.1.3. لماذا لا تكفي bytearray أيضًا

تُعد bytearray الشكل الصحيح -- مخزن واحد مُنمّط، وبايت واحد لكل عنصر، كل ذلك في كتلة متجاورة واحدة. وهي ما تعيده معظم واجهات برمجة تطبيقات الطرفيات الموجهة بالبايتات. وما ينقصها هو الحساب. فـ bytearray * 2 تكرّر المخزن بدلًا من مضاعفة كل قيمة، ولا يوجد معنى منطقي لـ bytearray + bytearray عنصرًا بعنصر.

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