OpenCV-basierte Programmoptimierung eingebettetes Linux-Betriebssystem

OpenCV-basierte Programmoptimierung eingebettetes Linux-Betriebssystem

Ich baue mit Buildroot mein eigenes Embedded Linux-Betriebssystem für Raspberry PI3. Dieses Betriebssystem wird zur Handhabung mehrerer Anwendungen verwendet, eine davon führt eine Objekterkennung auf Basis von OpenCV (v3.3.0) durch.

Ich habe mit Raspbian Jessy + Python begonnen, aber es stellte sich heraus, dass die Ausführung eines einfachen Beispiels sehr zeitaufwändig ist. Daher habe ich beschlossen, mein eigenes RTOS mit optimierten Funktionen + C++-Entwicklung statt Python zu entwerfen.

Ich dachte, dass mit diesen Optimierungen die 4 Kerne von RPI + der 1 GB RAM solche Anwendungen bewältigen würden. Das Problem ist, dass selbst mit diesen Dingen die einfachsten Computer Vision-Programme viel Zeit in Anspruch nehmen.

Vergleich PC vs. Raspberry PI3

Dies ist ein einfaches Programm, das ich geschrieben habe, um eine Vorstellung von der Größenordnung der Ausführungszeit jedes Programmteils zu bekommen.

#include <stdio.h>
#include "opencv2/core.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"

#include <time.h>       /* clock_t, clock, CLOCKS_PER_SEC */

using namespace cv;
using namespace std;

int main()
{
    setUseOptimized(true);
    clock_t t_access, t_proc, t_save, t_total;

    // Access time.
    t_access = clock();
    Mat img0 = imread("img0.jpg", IMREAD_COLOR);// takes ~90ms
    t_access = clock() - t_access;

    // Processing time
    t_proc = clock();
    cvtColor(img0, img0, CV_BGR2GRAY); 
    blur(img0, img0, Size(9,9));// takes ~18ms
    t_proc = clock() - t_proc;

    // Saving time
    t_save = clock();
    imwrite("img1.jpg", img0);
    t_save = clock() - t_save;

    t_total = t_access + t_proc + t_save;

    //printf("CLOCKS_PER_SEC = %d\n\n", CLOCKS_PER_SEC);

    printf("(TEST 0) Total execution time\t %d cycles \t= %f ms!\n", t_total,((float)t_total)*1000./CLOCKS_PER_SEC);
    printf("---->> Accessing  in\t %d cycles \t= %f ms.\n", t_access,((float)t_access)*1000./CLOCKS_PER_SEC);
    printf("---->> Processing in\t %d cycles \t= %f ms.\n", t_proc,((float)t_proc)*1000./CLOCKS_PER_SEC);
    printf("---->> Saving     in\t %d cycles \t= %f ms.\n", t_save,((float)t_save)*1000./CLOCKS_PER_SEC);

    return 0;
}

Ergebnisse der Ausführung auf einem i7-PC Bildbeschreibung hier eingeben

Ergebnisse der Ausführung auf Raspberry PI (generiertes Betriebssystem von Buildroot) Bildbeschreibung hier eingeben

Wie Sie sehen, gibt es einen großen Unterschied. Was ich brauche, ist jedes einzelne Detail zu optimieren, damit dieses Beispielwird bearbeitetSchritt erfolgt in nahezu Echtzeit beiin maximal 15ms Zeit.

Meine Frage betrifft:

  • Wie kann ich mein Betriebssystem optimieren, sodass es rechenintensive Anwendungen bewältigen kann und wie kann ich die Prioritäten der einzelnen Teile steuern?
  • Wie kann ich die 4 Kerne des RPI3 voll ausnutzen, um die Anforderungen zu erfüllen?
  • Gibt es andere Möglichkeiten als OpenCV?
  • Sollte ich C statt C++ verwenden?
  • Gibt es Hardwareverbesserungen, die Sie empfehlen?

Antwort1

In Ordnung:

Wie kann ich mein Betriebssystem optimieren, sodass es rechenintensive Anwendungen bewältigen kann und wie kann ich die Prioritäten der einzelnen Teile steuern?

Zur allgemeinen Optimierung können Sie auf der Betriebssystemseite nicht viel tun, außer die normalen Dinge, wie z. B. sicherzustellen, dass im Hintergrund nur das ausgeführt wird, was Sie wirklich brauchen. Auf dem ursprünglichen Pi konnten Sie memmove()ähnliche Funktionen beschleunigen, indem LD_PRELOADSie eine Bibliothek namens „cofi“ verwendeten, die Assembler-optimierte Versionen dieser Funktionen bereitstellte, aber ich bin nicht sicher, ob das auf einem Pi 3 hilft.

Informationen zur Priorisierung finden Sie in den Manpages. Im Allgemeinen ist dies jedoch nur möglich, wenn Sie die Dinge parallelisieren (in Ihrem Fall scheint die offensichtliche Lösung darin zu bestehen, jeden Schritt als eigenen Prozess auszuführen und IPC (aus Leistungsgründen wahrscheinlich gemeinsam genutzten Speicher) zum Verschieben der Daten zwischen ihnen zu verwenden).

Beachten Sie in Bezug auf die Ergebnisse, die Sie aus Ihrem Testprogramm zitiert haben, insbesondere, dass die Verarbeitungs- und Speicherschritte auf dem Pi beide etwa 10-mal langsamer sind, während der Zugriffsschritt nur etwa 5-mal langsamer ist, und diese Zahlen stimmen mit einer groben Schätzung dessen überein, was ich erwarten würde, wenn ich einen Pi 3 mit einem generischen PC vergleiche, der weniger als ein Jahr alt ist. Die CPU im Pi ist mit ziemlicher Sicherheit erheblich langsamer als die, auf der Sie den PC-Test ausgeführt haben (und wenn Sie die Dinge überhaupt nicht parallelisiert haben, wird die Lücke noch größer, da die meisten modernen x86-CPUs einen einzelnen Kern allein bei voller Auslastung viel schneller ausführen können als alle ihre Kerne bei voller Auslastung), und das wird Auswirkungen haben. Die ARM ISA unterscheidet sich außerdem erheblich von der x86 ISA (ARM führt im Vergleich zu x86 tendenziell weniger pro Zyklus aus, muss dafür aber normalerweise nicht so häufig auf den RAM zugreifen und verursacht Fehlschläge bei der Verzweigungsvorhersage normalerweise nicht so kostspielig wie x86). Daher ist jeder Code, der für die Anordnung der Dinge durch GCC auf einem PC optimiert ist, auf einem Pi nicht so optimal.

Ich weiß auch nicht, welche Kamera Sie verwenden, aber ich vermute, dass Sie bessere Zeiten erzielen könnten, wenn Sie die Auflösung der zu verarbeitenden Bilder verringern. Auch die Erfassungszeit können Sie wahrscheinlich verkürzen, wenn Sie die Verwendung komprimierter Formate vermeiden (und wenn Sie keine verlustbehaftete Komprimierung verwenden, spielt die Auflösung keine so große Rolle).

Wie kann ich die 4 Kerne des RPI3 voll ausnutzen, um die Anforderungen zu erfüllen?

Parallelisierung in Ihrem eigenen Code. Sie müssen nur sicherstellen, dass SMP in Ihrem Kernel aktiviert ist (und wenn Sie die offizielle Konfiguration der RPi Foundation verwenden, sollte dies der Fall sein) und dann versuchen, die Dinge parallel auszuführen. Ich bin nicht sicher, wie viel OpenCV selbst zur Parallelisierung beiträgt, aber Sie sollten sich auch OpenMP ansehen (es bietet eine relativ einfache Möglichkeit, Iterationen in Schleifen zu parallelisieren, die nicht voneinander abhängig sind).

Gibt es andere Möglichkeiten als OpenCV?

Das könnte sein, aber jeder hat sich auf OpenCV standardisiert, daher würde ich vorschlagen, das zu verwenden (Sie werden leichter technische Hilfe bei der Implementierung bekommen, weil es jeder verwendet).

Sollte ich C statt C++ verwenden?

Das hängt davon ab, wie Sie die Dinge verwenden. Während es viel einfacher ist, langsamen Code in C++ als in C zu schreiben, ist es in keiner der beiden Sprachen schwieriger, schnellen Code zu schreiben. Viele der Optimierungstechniken sind in beiden Sprachen ziemlich ähnlich (z. B. alles beim Start vorab zuzuweisen, damit Sie malloc()in kritischen Abschnitten nicht aufrufen, oder das Vermeiden des Aufrufs von stat()). Im Fall von C++ sollten Sie es jedoch std::stringwie die Pest vermeiden, da es malloc()überall aufruft und daher wahnsinnig langsam ist (ich habe Konvertierungen gesehen, die von std::stringin C-artige Zeichenfolgen wechseln und die Leistung in einigen Fällen um über 40 % verbessern).

Gibt es Hardwareverbesserungen, die Sie empfehlen?

Unter der Annahme, dass Sie die Hardwarekosten niedrig halten möchten und nur begrenzt Platz haben (deshalb die Wahl des Raspberry Pi), fallen mir eigentlich keine ein. Der Pi (in allen seinen Versionen) verwendet ein SoC, das in dieser Preisklasse ziemlich einzigartig für Computer Vision geeignet ist. Wenn Sie bereit sind, etwas Größeres und etwas Teureres zu kaufen, würde ich Ihnen ein NVIDIA Jetson-Board empfehlen (sie verwenden ein Tegra-SoC mit einer Quadro-äquivalenten GPU mit 192 integrierten CUDA-Kernen, sodass Ihre Verarbeitungsarbeitslast wahrscheinlich viel schneller ausgeführt werden kann), aber Buildroot dort zum Laufen zu bringen, ist deutlich aufwändiger als auf einem Pi.

Änderungen als Reaktion auf Kommentare:

Parallelisierung auf Prozessebene ist nicht dasselbe wie Multithreading, sie ist völlig anders (der größte Unterschied besteht in der Art und Weise, wie Ressourcen gemeinsam genutzt werden. Standardmäßig teilen sich Threads alles, Prozesse nichts). Wenn viele Verarbeitungsvorgänge erforderlich sind, ist die prozessbasierte Parallelisierung im Allgemeinen (normalerweise) besser geeignet, da es einfacher ist, effizienten Code zu schreiben, ohne sich um die Threadsicherheit sorgen zu müssen.

Was die Optionen betrifft, können die beiden, die Sie erwähnt haben, einen großen Einfluss auf die Systemleistung haben, aber beide sind letztendlich Kompromisse zwischen Durchsatz und Latenz. Das Präemptionsmodell steuert, wie Dinge, die im Kernelmodus ausgeführt werden (wie Systemaufrufe), neu geplant werden können. Die drei Optionen sind:

  1. Keine Präemption: Das bedeutet im Wesentlichen, dass nichts, was im Kernelmodus läuft, unterbrochen werden kann. Es entspricht dem Verhalten von SVR4 und 4.4BSD sowie der Funktionsweise der meisten anderen älteren UNIX-Systeme. Es ist sehr gut für den Durchsatz, aber sehr schlecht für die Latenz, daher wird es im Allgemeinen nur auf großen Servern mit vielen CPUs verwendet (mehr CPUs bedeuten, dass es wahrscheinlicher ist, dass etwas ausgeführt wird, das präemptiert werden kann).
  2. Freiwillige Präemption: Dadurch kann jede Funktion im Kernel Speicherorte definieren, an denen sie neu geplant werden kann. Dies ist die Einstellung, die die meisten auf Desktops ausgerichteten Linux-Distributionen verwenden, da sie ein gutes Gleichgewicht zwischen Durchsatz und Latenz bietet.
  3. Vollständige Präemption: Das bedeutet, dass (fast) jeder Code im Kernel (fast) jederzeit unterbrochen werden kann. Das ist nützlich für Systeme, die eine sehr geringe Latenz in Bezug auf Eingaben und externe Ereignisse benötigen, wie z. B. Systeme, die für Echtzeit-Multimediaarbeiten verwendet werden. Für den Durchsatz ist das absolut schrecklich, aber die Latenz ist unschlagbar.

Die Timerfrequenz ist dagegen viel einfacher zu erklären. Sie steuert den längsten Zeitraum, in dem etwas ohne Unterbrechung ausgeführt werden kann, wenn etwas anderes darauf wartet, ausgeführt zu werden. Höhere Werte führen zu einem kürzeren Zeitraum (geringere Latenz und geringerer Durchsatz), niedrigere Werte zu einem längeren Zeitraum (höhere Latenz und höherer Durchsatz). Für einen allgemeinen Start würde ich vorschlagen, das Präemptionsmodell auf freiwillig und die Timerfrequenz auf 300 Hz einzustellen und dann zunächst mit der Änderung der Timerfrequenz zu experimentieren (da dies normalerweise einen sichtbareren Einfluss hat).

Ob sich der Movidius NCS lohnt, hängt davon ab, mit wie vielen Daten Sie arbeiten müssen, da die Bandbreite durch die USB-Verbindung eingeschränkt wird (der Pi hat nur einen einzigen USB 2.0-Controller, sodass Sie nicht nur auf weniger als ein Zehntel der Bandbreite beschränkt sind, für die der Movidius ausgelegt ist, sondern Sie müssen den Bus auch mit mindestens dem Ethernet-Adapter teilen, was sowohl Ihre Latenz als auch Ihren Durchsatz beeinträchtigt). Wenn Sie nur einzelne Frames von 1920 x 1080 mit 32-Bit-Farbe bei niedriger Rate verarbeiten, ist dies möglicherweise machbar, aber wenn Sie dasselbe Video bei voller Framerate streamen müssen, werden Sie wahrscheinlich auf Latenzprobleme stoßen. Wenn Sie sich für die Verwendung eines solchen entscheiden, stellen Sie sicher, dass Sie einen Hub mit Stromversorgung dafür bekommen (sonst könnten Sie Probleme damit haben, dass er versucht, mehr Strom zu ziehen, als der Pi bereitstellen kann).

verwandte Informationen