6.1. Warum Arrays

Die Klasse Image ist das richtige Werkzeug für Pixelarbeit, weil jede ihrer Methoden direkt auf dem nativen Pixelpuffer der Kamera in einem einzigen schnellen Aufruf arbeitet. Das meiste, was die Anwendung mit einem Einzelbild macht – Schwellenwertbildung, Blob-Suche, AprilTag-Erkennung, Kantenfilter – lebt bereits dort.

Was die Bildbibliothek nicht bereitstellt, ist der Rest der numerischen Arbeit, auf die eine OpenMV-Anwendung stößt:

  • Sensorpuffer, die keine Pixel sind – ADC-Samples, Achsen einer IMU (Inertialmesseinheit), Mikrofonaudio,

  • abgeleitete Zahlen aus dem Bild, die keine eingebaute Methode zurückgibt – eine Histogrammspalte, eine benutzerdefinierte Mischung zweier Einzelbilder, eine Pro-Pixel-Transformation, die der Katalog nicht abdeckt,

  • kleine lineare Algebra – die Kalibriermatrix, die das Objektiv entzerrt, die Rotation, die die IMU fusioniert,

  • Signalverarbeitungsmathematik – der Frequenzgehalt eines Vibrationspuffers, die Glättung der Ausgabe eines Sensors, ein Merkmalsvektor, den ein Klassifizierer als Eingabe möchte.

All diese möchten dieselbe Form: einen Puffer von Zahlen mit einer Operation, die auf jedes Element angewendet wird. Eine Python-for-Schleife ist der naheliegende Weg, das zu schreiben:

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

Die Schleife funktioniert. Sie ist auch langsam. Python ist eine interpretierte Sprache, und jede Iteration einer Python-Schleife trägt die Kosten dafür, den Interpreter einmal auszuführen: samples nachschlagen, Element i lesen, multiplizieren, zurückschreiben, den Schleifenzähler weiterschalten, die Schleifenbedingung prüfen. Bei einem Puffer von tausend Sensor-Samples summieren sich diese Interpreterkosten auf zehn Millisekunden für etwas, das im Grunde eine schnelle Operation ist.

Dieser Overhead beißt jedes Mal, wenn ein Skript einen Puffer erreicht. Ein QVGA-Graustufen-Einzelbild hat 76.800 Pixel; ein Beschleunigungssensor liefert bei 100 Hz hundert Dreiachsen-Samples pro Sekunde; ein Mikrofon füllt alle 64 ms einen 1024-Sample-Puffer. Eine reine Python-for-Schleife über eines davon verwandelt eine Aufgabe, die ein paar Mikrosekunden dauern sollte, in eine, die zehn Millisekunden dauert – und bei einem bildgroßen Puffer noch einmal rund zehnmal länger.

6.1.1. Bibliotheksfunktionen sind schneller als Schleifen

Die Lösung besteht darin, die Operation als einen einzigen Funktionsaufruf gegen den gesamten Puffer auszudrücken, statt als Python-Schleife über seine Elemente. numpy ist genau das: eine Bibliothek von Array-Mathematik, bei der jede Operation eine bereits optimierte Funktion ist, die den Puffer einmal von Anfang bis Ende durchläuft. np.multiply(samples, cal) multipliziert jedes Element von samples mit cal innerhalb eines einzigen Aufrufs – dieselbe Arithmetik, die die Schleife durchführte, ohne die Interpreterkosten pro Iteration. Dieselbe 1000-elementige Multiplikation, die als Python-Schleife zehn Millisekunden dauerte, dauert als numpy-Aufruf zehn Mikrosekunden.

Das ist der Handel, den numpy durchweg anbietet: Summe, Mittelwert, sin, exp, Matrixmultiplikation, Signalverarbeitungsprimitive – jedes davon ist eine einzige Bibliotheksfunktion, die auf einem ganzen Puffer auf einmal arbeitet. Der Kompromiss ist, dass die Daten im Array-Typ von numpy leben müssen und die Operation gegen dieses Array ausgedrückt werden muss, nicht gegen seine Elemente einzeln.

6.1.2. Warum eine Liste nicht ausreicht

Eine Python-list kann nicht einspringen. Eine Liste kann jede Mischung von Objekten halten – Integer, Floats, Strings, andere Listen – und eine Bibliotheksfunktion, die sie liest, muss trotzdem jeden Slot anschauen, um herauszufinden, was darin ist, und den Wert herausziehen, bevor irgendeine Arithmetik geschieht. Dieser Overhead pro Slot ist genau die Kosten, die die Python-Schleife zahlt. Listen sind die falsche Wahl für schnelle Array-Mathematik.

6.1.3. Warum bytearray auch nicht genügt

Ein bytearray ist die richtige Form – ein typisierter Puffer, ein Byte pro Element, alles in einem zusammenhängenden Block. Es ist das, was die meisten byteorientierten Peripherie-APIs zurückgeben. Was ihm fehlt, ist die Mathematik. bytearray * 2 wiederholt den Puffer, anstatt jeden Wert zu verdoppeln, und es gibt keine sinnvolle Bedeutung für bytearray + bytearray Element für Element.

Die Datenstruktur, die einen typisierten Puffer mit elementweiser Mathematik kombiniert, ist das ndarray. Was sich in der Box befindet und wie jedes Feld das Verhalten des schnellen Pfads formt, sind die Grundlagen, auf denen der Rest dieses Kapitels ruht.