1. Was ist OpenCV?
OpenCV ist eine freie Programmbibliothek für Bildverarbeitung und
Computer Vision. In Python wird sie über das Modul cv2
eingebunden.
Bildverarbeitung ist häufig die Vorstufe der Bilderkennung. Dabei werden Bilddaten beispielsweise geglättet, segmentiert oder in eine Maske umgewandelt. Anschließend kann das Programm beschreiben, welche Objekte vorhanden sind.
2. Bild laden und anzeigen
from pathlib import Path
import cv2
ordner = Path(__file__).resolve().parent
bildpfad = ordner / "arbeitsplatte_mit_objekten.png"
bild = cv2.imread(str(bildpfad))
if bild is None:
raise FileNotFoundError(f"Bild nicht gefunden: {bildpfad}")
cv2.imshow("Geladenes Bild", bild)
cv2.waitKey(0)
cv2.destroyAllWindows()
| Befehl | Bedeutung |
|---|---|
cv2.imread(...) |
Lädt und decodiert die Bilddatei. |
cv2.imshow(...) |
Zeigt ein Bild in einem OpenCV-Fenster. |
cv2.waitKey(0) |
Wartet auf einen Tastendruck. |
cv2.destroyAllWindows() |
Schließt alle OpenCV-Fenster. |
3. Welche Daten enthält bild?
Ein geladenes Farbbild ist ein dreidimensionales NumPy-Array:
print(type(bild))
print(bild.shape)
print(bild.dtype)
Eine typische Ausgabe lautet:
<class 'numpy.ndarray'>
(662, 663, 3)
uint8
| Angabe | Bedeutung |
|---|---|
662 |
Bildhöhe in Pixeln |
663 |
Bildbreite in Pixeln |
3 |
Drei Farbkanäle: Blau, Grün, Rot |
uint8 |
Ganzzahlen von 0 bis 255 |
Ein einzelnes Pixel
pixel = bild[y, x]
print(pixel)
OpenCV verwendet die Reihenfolge BGR:
[Blau, Gruen, Rot]
schwarz = [0, 0, 0]
weiss = [255, 255, 255]
rot = [0, 0, 255]
gruen = [0, 255, 0]
blau = [255, 0, 0]
bild[y, x] adressiert.
Die Reihenfolge ist also zuerst die Zeile und danach die Spalte.
Warum erscheinen drei Punkte?
array([
[[254, 253, 253], [253, 253, 253], ...],
...
])
Die Zeichen ... bedeuten, dass NumPy einen großen Teil der Werte
nur in der Anzeige auslässt. Die Daten sind weiterhin vollständig im Array
vorhanden.
Eine vollständige Bildzeile
bild[3]
liefert die vierte Bildzeile, weil Python bei null zu zählen beginnt.
4. Bilder verkleinern oder vergrößern
Skalierung in Prozent
prozent = 50
faktor = prozent / 100
kleines_bild = cv2.resize(
bild,
None,
fx=faktor,
fy=faktor,
interpolation=cv2.INTER_AREA
)
Bei prozent = 50 werden Breite und Höhe jeweils halbiert.
Für das Verkleinern ist cv2.INTER_AREA meist gut geeignet.
Wiederverwendbare Funktion
def bild_skalieren(bild, prozent):
faktor = prozent / 100
interpolation = (
cv2.INTER_AREA
if prozent < 100
else cv2.INTER_LINEAR
)
return cv2.resize(
bild,
None,
fx=faktor,
fy=faktor,
interpolation=interpolation
)
anzeige = bild_skalieren(bild, 50)
5. Formen erkennen
Nach der Segmentierung werden mit cv2.findContours() die äußeren
Umrisse der Objekte bestimmt.
konturen, _ = cv2.findContours(
maske,
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE
)
Für jede Kontur können folgende Daten berechnet werden:
| Funktion | Ergebnis |
|---|---|
cv2.contourArea(kontur) |
Fläche in Pixel² |
cv2.arcLength(kontur, True) |
Umfang in Pixeln |
cv2.boundingRect(kontur) |
x, y, Breite und Höhe |
cv2.moments(kontur) |
Mittelpunkt der Kontur |
cv2.approxPolyDP(...) |
Vereinfachte Kontur und Eckenzahl |
Grundidee der Formerkennung
if kreisfoermigkeit > 0.84:
form = "Kreis"
elif anzahl_ecken == 3:
form = "Dreieck"
elif anzahl_ecken == 4:
seitenverhaeltnis = breite / float(hoehe)
if 0.90 <= seitenverhaeltnis <= 1.10:
form = "Quadrat"
else:
form = "Rechteck"
else:
form = "Unbekannt"
6. Farben erkennen
Für die Farberkennung eignet sich der HSV-Farbraum besser als BGR, weil Farbton, Sättigung und Helligkeit getrennt vorliegen.
hsv = cv2.cvtColor(
bild,
cv2.COLOR_BGR2HSV
)
Farbige Flächen auswählen
maske = cv2.inRange(
hsv,
np.array([0, 80, 50]),
np.array([179, 255, 255])
)
Damit werden deutlich gesättigte Farben weiß markiert. Weißer Hintergrund, schwarze Linien und Rasterlöcher bleiben überwiegend schwarz.
Farbnamen aus dem Farbton bestimmen
def farbe_bestimmen(farbton):
if farbton < 10 or farbton >= 170:
return "Rot"
if farbton < 40:
return "Gelb"
if farbton < 85:
return "Gruen"
if farbton < 130:
return "Blau"
if farbton < 165:
return "Violett"
return "Unbekannt"
7. Arbeitsplatte ausblenden
Methode A: Farbmaske
Für die gezeichneten farbigen Formen ist eine HSV-Maske besonders einfach. Kleine rote Striche des Arbeitsbereiches können über eine Mindestfläche verworfen werden.
for kontur in konturen:
flaeche = cv2.contourArea(kontur)
if flaeche < 2000:
continue
Methode B: Leeres Referenzbild
Bei einer realen, fest montierten Kamera kann ein Foto der leeren Platte vom aktuellen Bild abgezogen werden.
differenz = cv2.absdiff(
bild_mit_objekten,
bild_leer
)
grau = cv2.cvtColor(
differenz,
cv2.COLOR_BGR2GRAY
)
_, maske = cv2.threshold(
grau,
30,
255,
cv2.THRESH_BINARY
)
Dieses Verfahren kann auch schwarze, weiße oder graue Objekte erkennen. Kamera, Platte und Beleuchtung sollten dabei möglichst unverändert bleiben.
8. Bereiche von der Erkennung ausschließen
Eine Bereichsmaske legt fest, welche Teile des Bildes ausgewertet werden. Weiße Bereiche sind erlaubt, schwarze Bereiche werden ignoriert.
Gesamtes Bild zunächst erlauben
hoehe, breite = bild.shape[:2]
bereichsmaske = np.full(
(hoehe, breite),
255,
dtype=np.uint8
)
Dobot-Standplatz ausschließen
cv2.rectangle(
bereichsmaske,
(245, 265),
(415, 435),
0,
-1
)
Mit der Farbmaske verbinden
maske = cv2.bitwise_and(
farbmaske,
bereichsmaske
)
Nur einen kreisförmigen Arbeitsbereich erlauben
bereichsmaske = np.zeros(
(hoehe, breite),
dtype=np.uint8
)
cv2.circle(
bereichsmaske,
(330, 330),
300,
255,
-1
)
9. Kompaktes Gesamtbeispiel
Dieses Programm lädt das Bild, erzeugt eine Farbmaske, schließt einen rechteckigen Bereich aus, erkennt Konturen und schreibt die Ergebnisse in die Thonny-Shell.
from pathlib import Path
import math
import cv2
import numpy as np
def farbe_bestimmen(farbton):
if farbton < 10 or farbton >= 170:
return "Rot"
if farbton < 40:
return "Gelb"
if farbton < 85:
return "Gruen"
if farbton < 130:
return "Blau"
if farbton < 165:
return "Violett"
return "Unbekannt"
ordner = Path(__file__).resolve().parent
bildpfad = ordner / "arbeitsplatte_mit_objekten.png"
bild = cv2.imread(str(bildpfad))
if bild is None:
raise FileNotFoundError(f"Bild nicht gefunden: {bildpfad}")
ergebnis = bild.copy()
hsv = cv2.cvtColor(bild, cv2.COLOR_BGR2HSV)
farbmaske = cv2.inRange(
hsv,
np.array([0, 80, 50]),
np.array([179, 255, 255])
)
hoehe, breite = bild.shape[:2]
bereichsmaske = np.full(
(hoehe, breite),
255,
dtype=np.uint8
)
# Beispiel: Dobot-Standplatz ausschließen
cv2.rectangle(
bereichsmaske,
(245, 265),
(415, 435),
0,
-1
)
maske = cv2.bitwise_and(
farbmaske,
bereichsmaske
)
kernel = cv2.getStructuringElement(
cv2.MORPH_ELLIPSE,
(5, 5)
)
maske = cv2.morphologyEx(
maske,
cv2.MORPH_OPEN,
kernel
)
maske = cv2.morphologyEx(
maske,
cv2.MORPH_CLOSE,
kernel
)
konturen, _ = cv2.findContours(
maske,
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE
)
objekte = []
for kontur in konturen:
flaeche = cv2.contourArea(kontur)
if flaeche < 2000:
continue
umfang = cv2.arcLength(kontur, True)
if umfang == 0:
continue
naeherung = cv2.approxPolyDP(
kontur,
0.02 * umfang,
True
)
ecken = len(naeherung)
x, y, w, h = cv2.boundingRect(kontur)
momente = cv2.moments(kontur)
if momente["m00"] != 0:
mitte_x = int(momente["m10"] / momente["m00"])
mitte_y = int(momente["m01"] / momente["m00"])
else:
mitte_x = x + w // 2
mitte_y = y + h // 2
kreisfoermigkeit = (
4 * math.pi * flaeche / (umfang * umfang)
)
if kreisfoermigkeit > 0.84:
form = "Kreis"
elif ecken == 3:
form = "Dreieck"
elif ecken == 4:
verhaeltnis = w / float(h)
form = (
"Quadrat"
if 0.90 <= verhaeltnis <= 1.10
else "Rechteck"
)
else:
form = "Unbekannt"
objektmaske = np.zeros(
maske.shape,
dtype=np.uint8
)
cv2.drawContours(
objektmaske,
[kontur],
-1,
255,
-1
)
mittlere_hsv_farbe = cv2.mean(
hsv,
mask=objektmaske
)
farbe = farbe_bestimmen(
mittlere_hsv_farbe[0]
)
objekte.append({
"kontur": kontur,
"farbe": farbe,
"form": form,
"mitte_x": mitte_x,
"mitte_y": mitte_y,
"breite": w,
"hoehe": h,
"flaeche": flaeche,
"umfang": umfang
})
objekte.sort(
key=lambda objekt: objekt["mitte_x"]
)
print()
print("AUSWERTUNG DER ARBEITSPLATTE")
print("=" * 92)
print(
f'{"Nr.":>3} '
f'{"Farbe":<10} '
f'{"Form":<12} '
f'{"Mitte x":>8} '
f'{"Mitte y":>8} '
f'{"Breite":>8} '
f'{"Hoehe":>8} '
f'{"Flaeche":>10}'
)
print("-" * 92)
for nummer, objekt in enumerate(objekte, start=1):
print(
f'{nummer:>3} '
f'{objekt["farbe"]:<10} '
f'{objekt["form"]:<12} '
f'{objekt["mitte_x"]:>8} '
f'{objekt["mitte_y"]:>8} '
f'{objekt["breite"]:>8} '
f'{objekt["hoehe"]:>8} '
f'{objekt["flaeche"]:>10.1f}'
)
print("-" * 92)
print(f"Anzahl erkannter Objekte: {len(objekte)}")
cv2.imshow("Maske", maske)
cv2.imshow("Original", bild)
cv2.waitKey(0)
cv2.destroyAllWindows()
10. Sinnvolle nächste Schritte
- Pixelmittelpunkte den Lochkoordinaten der Arbeitsplatte zuordnen.
- Pixelabstände in Millimeter umrechnen.
- Perspektivische Verzerrungen der Kamera korrigieren.
- Nur Objekte innerhalb der Dobot-Reichweite berücksichtigen.
- Erkannte Objektkoordinaten in Dobot-Koordinaten umrechnen.
- Später reale Kamerabilder statt gezeichneter Testbilder verwenden.