OpenCV mit CUDA einrichten: GPU-beschleunigte Bildverarbeitung

Von Helmut Artmeier  •   4 Minuten gelesen

Zwei Hände setzen eine GPU-Karte mit Axiallüfter in einen geöffneten Industrie-PC ein (KI-generiertes Symbolbild)

OpenCV mit CUDA einrichten: GPU-beschleunigte Bildverarbeitung

Symbolbild, KI-generiert (gpt-image-1)

Die OpenCV-Pakete aus pip und aus den Ubuntu-Quellen sind ohne CUDA gebaut. Wer in der Bildverarbeitung die GPU nutzen will, kommt deshalb um eine eigene Übersetzung aus dem Quellcode nicht herum. Am Ende dieser Anleitung steht ein OpenCV, dessen cv2.cuda-Module verfügbar sind, und ein Benchmark, der zeigt, ob sich der Aufwand für die eigene Pipeline überhaupt gelohnt hat. Woran es typischerweise scheitert: an einer falsch gesetzten Compute Capability und an der Erwartung, dass jede Operation schneller wird.

Vorher klären: Lohnt sich die GPU für diese Pipeline?

Nicht jede Operation profitiert. Der Weg vom Hauptspeicher in den GPU-Speicher und zurück kostet Zeit, und bei einer einzelnen kleinen Operation frisst dieser Transfer den Gewinn vollständig auf. Tragfähig wird es bei hochauflösenden Bildern, bei mehreren aufeinander folgenden Filtern und dann, wenn der Frame ohnehin schon auf der GPU liegt — etwa weil danach ein Netz darauf rechnet.

Die Regel dahinter: Es zählt nicht die einzelne Operation, sondern wie lange ein Frame am Stück auf der GPU bleibt. Für Resize und Crop auf Full-HD ist die CPU schnell genug — und spart die Bauerei.

Voraussetzungen

  • Ubuntu 22.04 LTS, NVIDIA-Treiber und CUDA Toolkit installiert
  • nvcc --version und nvidia-smi liefern beide eine Ausgabe
  • 8 GB freier Speicher auf /tmp und etwas Geduld

Sind Treiber und Toolkit noch nicht eingerichtet, führen die Anleitungen zum NVIDIA-Treiber unter Linux und zur CUDA-Einrichtung dorthin.

Schritt 1: Abhängigkeiten installieren

sudo apt update
sudo apt install -y \
  cmake g++ wget unzip \
  libgtk2.0-dev pkg-config \
  libjpeg-dev libpng-dev libtiff-dev \
  libavcodec-dev libavformat-dev libswscale-dev \
  libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
  python3-dev python3-numpy

Schritt 2: Quellcode laden

Haupt- und Contrib-Repository müssen dieselbe Version haben — die CUDA-Module liegen im Contrib-Teil.

cd /tmp
VER=4.9.0
wget -O opencv.zip https://github.com/opencv/opencv/archive/$VER.zip
wget -O opencv_contrib.zip https://github.com/opencv/opencv_contrib/archive/$VER.zip
unzip -q opencv.zip
unzip -q opencv_contrib.zip

Schritt 3: Compute Capability ermitteln

Der häufigste Fehlgriff im ganzen Ablauf. Wird hier ein falscher Wert eingetragen, übersetzt der Build fehlerfrei — zur Laufzeit findet OpenCV dann aber kein passendes Gerät.

nvidia-smi --query-gpu=compute_cap --format=csv,noheader
# 8.6 = Ampere (z. B. RTX 3060), 8.9 = Ada (z. B. RTX 4060)

Schritt 4: Build konfigurieren

Den ermittelten Wert bei CUDA_ARCH_BIN eintragen. Innerhalb des Befehls stehen keine Kommentare — ein # hinter einem Zeilenumbruch-Backslash bricht den Aufruf ab.

mkdir -p /tmp/opencv-4.9.0/build
cd /tmp/opencv-4.9.0/build

cmake -D CMAKE_BUILD_TYPE=RELEASE \
  -D CMAKE_INSTALL_PREFIX=/usr/local \
  -D OPENCV_EXTRA_MODULES_PATH=/tmp/opencv_contrib-4.9.0/modules \
  -D WITH_CUDA=ON \
  -D CUDA_ARCH_BIN="8.6" \
  -D WITH_CUDNN=ON \
  -D OPENCV_DNN_CUDA=ON \
  -D ENABLE_FAST_MATH=ON \
  -D CUDA_FAST_MATH=ON \
  -D WITH_CUBLAS=ON \
  -D BUILD_opencv_python3=ON \
  -D BUILD_TESTS=OFF \
  -D BUILD_EXAMPLES=OFF \
  ..

Bevor es weitergeht, die CMake-Ausgabe lesen. Steht dort NVIDIA CUDA: NO, ist jede weitere Minute Rechenzeit verschenkt:

--   NVIDIA CUDA:                   YES (ver 12.3, CUFFT CUBLAS FAST_MATH)
--   cuDNN:                         YES (ver 8.x.x)
--   CUDA modules:                  cudaarithm cudabgsegm cudacodec ...

Schritt 5: Übersetzen und installieren

make -j$(nproc)
sudo make install
sudo ldconfig

Das dauert je nach Prozessor 20 bis 60 Minuten. Auf einem Industrie-PC mit vier Kernen ist eher das obere Ende zu erwarten — und ein -j$(nproc) kann bei knappem Arbeitsspeicher kippen. Als grober Anhalt: rund 1,5 GB RAM je paralleler Übersetzungseinheit. Bei 8 GB also besser make -j4 als make -j8.

Schritt 6: Prüfen, ob CUDA wirklich drin ist

import cv2

print(f"OpenCV: {cv2.__version__}")
print(f"CUDA-Geräte: {cv2.cuda.getCudaEnabledDeviceCount()}")

if cv2.cuda.getCudaEnabledDeviceCount() > 0:
    cv2.cuda.setDevice(0)
    print(f"GPU: {cv2.cuda.DeviceInfo(0).name()}")

Steht dort 0, wurde entweder das falsche Python gefunden oder der Build lief ohne CUDA.

Schritt 7: Bilder auf der GPU verarbeiten

import cv2

img_cpu = cv2.imread("frame.png")

img_gpu = cv2.cuda_GpuMat()
img_gpu.upload(img_cpu)

img_gray_gpu = cv2.cuda.cvtColor(img_gpu, cv2.COLOR_BGR2GRAY)
img_resized_gpu = cv2.cuda.resize(img_gray_gpu, (640, 640))

blur = cv2.cuda.createGaussianFilter(cv2.CV_8UC1, cv2.CV_8UC1, (5, 5), 1.5)
img_blur_gpu = blur.apply(img_resized_gpu)

img_result = img_blur_gpu.download()

Entscheidend ist die Reihenfolge: ein upload, dann alle Operationen, dann ein download. Wird zwischendurch heruntergeladen, um eine CPU-Funktion einzuschieben, ist der Vorteil weg.

Schritt 8: Nachmessen statt glauben

import time

N = 100

start = time.perf_counter()
for _ in range(N):
    gray = cv2.cvtColor(img_cpu, cv2.COLOR_BGR2GRAY)
    resized = cv2.resize(gray, (640, 640))
cpu_ms = (time.perf_counter() - start) / N * 1000

img_gpu.upload(img_cpu)
start = time.perf_counter()
for _ in range(N):
    gray_gpu = cv2.cuda.cvtColor(img_gpu, cv2.COLOR_BGR2GRAY)
    resized_gpu = cv2.cuda.resize(gray_gpu, (640, 640))
cv2.cuda.Stream_Null().waitForCompletion()
gpu_ms = (time.perf_counter() - start) / N * 1000

print(f"CPU: {cpu_ms:.2f} ms | GPU: {gpu_ms:.2f} ms | Faktor {cpu_ms/gpu_ms:.1f}")

Der Aufruf von waitForCompletion() gehört zwingend dazu: CUDA arbeitet asynchron, ohne diese Zeile misst man die Zeit, die das Absetzen der Befehle braucht — nicht ihre Ausführung. Auf einem 4K-Frame (3840 × 2160) liegt der Faktor auf einer Karte der RTX-3060-Klasse bei etwa 5. Das ist ein realistischer Wert für eine solche Kette; bei einer einzelnen Operation auf Full-HD kann er unter 1 fallen — die GPU ist dann langsamer als die CPU.

Wenn es nicht funktioniert

cv2.cuda existiert nicht. Das neue OpenCV liegt in /usr/local, pip hat aber sein eigenes Paket installiert. Mit python3 -c "import cv2; print(cv2.__file__)" prüfen, welches gefunden wird, und ein pip uninstall opencv-python opencv-contrib-python vorschalten.

getCudaEnabledDeviceCount() liefert 0. Meist zeigt die CMake-Ausgabe schon NVIDIA CUDA: NO — dann fehlte das Toolkit im Suchpfad. Seltener stimmt die Compute Capability nicht, dann läuft der Build durch und das Gerät wird trotzdem nicht erkannt.

Der Build bricht mit c++: fatal error: Killed ab. Das ist der Speicher, nicht der Code: Der Kernel hat den Compiler abgeschossen. Mit kleinerem -j-Wert erneut starten.

Und wo das hinführt

Steht die GPU-Pipeline, ist der nächste Schritt meist die Inferenz auf demselben Frame, ohne ihn zwischendurch zurückzuholen — der Weg dorthin führt über TensorRT und in der Anwendung über die Echtzeit-Objekterkennung am Edge. Welche Karte die Kette trägt, ist in der Auswahl der RTX-Karten für Inferenz aufgeschlüsselt.

Passende Hardware: PCIe-AI- und GPU-Karten sowie Box-PCs und Industrie-PCs als Plattform dafür.

Zurück zum Wiki-Inhaltsverzeichnis.

Zurück Weiter