Das automatische Auslösen der Raspberry Video Camera bleibt weiterhin das Thema. In den vorangegangenen Artikeln hatte ich die Kamerasteuerung per Bewegungssensor vorgestellt und dann die Farberkennung aus dem Videobild heraus. Beides sind brauchbare Trigger, wenn es darum geht, ein Objekt vor der Kamera zu erkennen, aber sie haben auch ihre spezifischen Nachteile. Wie wäre es nun, wenn wir die beiden Methoden kombinieren, von den jeweiligen Vorteilen profitieren, Nachteile ausgleichen und so die Objekterkennung noch treffsicherer machen? In diesem Artikel kombiniere ich die Farbauswertung mit den Signalen des PIR-Bewegungssensors.
Zuerst aber wieder ein Oachkatzl-Video. Mehr davon gibts in meinem YouTube-Kanal.Zur Wahrung deiner Privatsphäre wird erst eine Verbindung zu YouTube hergestelt, wenn du den Abspielbutton betätigst.
Unzulänglichkeiten der bisherigen Trigger
Wenn es darum geht, automatische Kameraauslöser zu beurteilen, muss man immer stark auf das jeweilige Umfeld schauen. Vor einem sehr statischen Hintergrund kann ein Hardware-Bewegungssensor ausgezeichnete Dienste leisten. Und eine Farberkennung ist immer dann gut geeignet, wenn das Zielobjekt sich farblich gut vom Hintergrund unterscheidet. Mein Umfeld stellt sich – wie auch im Video zu sehen ist – folgendermaßen dar: Eichhörnchen-Erkennung im Freien, mit einem Baum als Hintergrund, dessen Äste sich gelegentlich im Wind bewegen, Änderung der Lichtsituation über den Tag, zwei unterschiedliche Eichhörnchen-Farben, aber gleichbleibender Objekt-Abstand.
PIR-Bewegungssensor
Der Bewegungssensor schwächelt naturgemäß da, wo Bewegung auftritt, die nicht vom Zielobjekt – also dem Eichhörnchen – stammt. Das sind hier die Äste, die sich im Wind bewegen und Gegenlichtsituationen bei bestimmten Sonnenständen, die den PIR-Sensor austricksen. Eine weitere Unzulänglichkeit liegt darin, dass Eichhörnchen beim Fressen gerne mal still sitzen und der Bewegungsmelder mangels Bewegung die Videoaufzeichnung zu früh abschaltet. Der Bewegungssensor erkennt zwar sehr schnell, aber oft falsch.
Farbauswertung
Eichhörnchen anhand ihrer Fellfarbe zu erkennen, funktioniert sehr gut bei den rotbraunen Eichhörnchen. Schwieriger ist es bei den dunkelbraunen, da sich ihre Farbe auch im Hintergrund bei den Ästen der Kiefer wiederfindet. Da die Bilder nur im Sekundenabstand ausgewertet werden, ist diese Erkennung langsamer als der PIR-Sensor, dafür bleibt der Trigger auch aktiv, wenn sich das Eichhörnchen im Bild nicht bewegt.
Wie kann eine Kombination der beiden Methoden die Erkennung verbessern?
Dafür gibt es zwei Ansätze:
Regel 1: Die Videoaufnahme wird nur dann gestartet, wenn beide Sensoren ein Signal geben
Die Unzulänglichkeit beider Erkennungsmethoden ist, dass viele falsch positive Signale generiert werden. Durch einen Doubble-Check, quasi ein Vier-Augen-Prinzip sollen nur noch die Signale an die Kamera weitergegeben werden, die von beiden Sensoren bestätigt werden. So würden bewegte Äste zwar den PIR-Bewegungssensor auslösen, aber nicht die Farberkennung. Und umgekehrt braucht die Erkennung von vielen dunkelbraunen Pixeln im Bild erst die Bestätigung durch den Bewegungsmelder, um ein gültiges Signal zu erzeugen.
Regel 2: Die Videoaufzeichnung wird erst gestoppt, wenn über eine gewisse Zeit keiner der beiden Sensoren mehr ein Signal liefert
So lange bei laufendem Video noch einer der beiden Sensoren ein Signal liefert, bleibt die Kamera an. So darf der Bewegungssensor ruhig ausgehen, wenn sich das Eichhörnchen nicht bewegt. So lange die Fellfarbe noch erkannt wird, läuft die Kamera weiter.
Python-Programm für den Kombinationstrigger (analyze2.py
)
Am Video-Aufzeichnungsprogramm (record2.py
) brauchen wir keine Änderung vorzunehmen, aus dem Farbanalyse-Programm analyze.py
machen wir aber eine neue modifizierte Version analyze2.py
. Das komplette Programm kommt nochmal in einem Stück am Ende des Artikels, hier jetzt erst mal die neuen und modifizierten Programmteile.
class imageLoader
Dieser Programmteil bleibt unverändert, das sekündliche Einladen der exportierten Videobilder funktioniert genauso wie vorher.
class pir
Das ist die neue Klasse, die den PIR-Bewegungssensor bedient:
class pir:
def __init__(self, pin):
self.timestamp = ''
GPIO.setmode(GPIO.BOARD)
# Set pin as input
GPIO.setup(pin,GPIO.IN)
# Bind event to callbac function
GPIO.add_event_detect(pin, GPIO.BOTH, self.PIR_callback)
def PIR_callback(self, pin):
triggertime = datetime.datetime.now()
if GPIO.input(pin) == 1:
self.timestamp = triggertime.strftime("%Y-%m-%d-%H-%M-%S")
print('PIR-Trigger active')
else:
print('PIR-Trigger cleared')
self.timestamp = ''
def triggered(self):
return (self.timestamp)
Der Programmteil ist ziemlich simpel und funktioniert im Prinzip genauso, wie wir es von motioninterrupt.py
her kennen. Im Konstruktor wird der GPIO-Anschluss konfiguriert und dann ein Event definiert, der bei einer Signaländerung am GPIO-Pin eine Callback-Funktion aufruft. Die Callback-Funktion macht nicht viel: außer ein paar Prints auf die Konsole, wird nur im Falle eines Signals ein entsprechender Timestamp in self.timestamp
gespeichert – mehr nicht. Zusätzlich legen wir eine Methode triggered
an, mit der der Inhalt von self.timestamp ausgelesen werden kann. Und der ist ein leerer String, wenn keine Bewegung erkannt wird und im positiven Fall der Timestamp zu dem die Erkennung stattgefunden hat.
class imageAnalyzer
Den Imageanalyzer kennen wir bereits, das ist das Farbauswerteprogramm. Das bleibt weitgehend gleich und wird nur dahingehend verändert, dass die Methode detect
nun nicht mehr den Triggerstatus zurückliefert, sondern ihn intern als Timestamp speichert, genauso, wie pir
das auch macht. Analog zum Bewegungssensor wird auch ein kleiner Timeout mitgeführt, während dem die Farberkennung auch mal ausbleiben kann, ohne dass das Triggersignal auf low geht Und genauso wie bei pir
gibt es eine Methode, mit der der Erkennungsstatus abgefragt werden kann.
class imageAnalyzer:
def __init__(self):
self.avgPix = -1 # average number of detected pixel
self.avgVola = -1 # average volatility (gap between number of detected pixel and their average value)
self.night = False # indicator for day or night
self.timestamp = '' # indicates current active object detection
self.lastTrigger = datetime.datetime.now() # stores last time of object detection
def detect(self, img, ts):
now = datetime.datetime.now()
# Detect whether day or night
# Convert to grayscale
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# Brightness is the average of all pixels
brightness = np.average(gray)
# Change from night to day or vice versa
if self.night and brightness > sunrise: self.night = False
if not self.night and brightness < sunset: self.night = True
# Find matching pixel (for red and brown squrrel) in image
# Convert BGR to HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# define range of red color in HSV
lower_value = np.array([0,70,110])
upper_value = np.array([11,170,220])
maskRed = cv2.inRange(hsv, lower_value, upper_value)
lower_value = np.array([165,70,110])
upper_value = np.array([179,170,220])
maskRed2 = cv2.inRange(hsv, lower_value, upper_value)
# define range of brown color in HSV
lower_value = np.array([12,40,220])
upper_value = np.array([17,65,255])
maskBrown = cv2.inRange(hsv, lower_value, upper_value)
lower_value = np.array([125,30,45])
upper_value = np.array([170,65,110])
maskBrown2 = cv2.inRange(hsv, lower_value, upper_value)
# Mask for red and brown
mask = maskRed+maskRed2+maskBrown+maskBrown2
pixDetected = cv2.countNonZero(mask)
# special case: average values not yet computed
if self.avgPix == -1:
self.avgPix = pixDetected
self.avgVola = minPix * sigma
# calc threshold (pixDetected must be higher for a valid object detection)
movingBand = self.avgVola * sigma
if movingBand < minPix:
movingBand = minPix
threshold = self.avgPix + movingBand
print("{0} Pix: {1:5d} Avg: {2:5.0f} Vola: {3:5.0f} Thresh: {4:5.0f} Bright: {5:3.0f} {6}{7}" \
.format(ts, pixDetected, self.avgPix, self.avgVola, threshold, brightness, \
"-" if self.night==True else "", \
"T" if pixDetected >= threshold else ""))
# cv2.imwrite('result.jpg',mask)
# cv2.imwrite('result1.jpg',img)
# no object detection because it is night or amount of detected pixels is below threshold
if self.night or pixDetected < threshold:
# clearing of trigger signal if timed out
if self.timestamp:
if (now - self.lastTrigger).total_seconds() > objectDetTriggerTO:
self.timestamp = ''
print('Object-Detection-Trigger cleared')
else:
# update average values
self.avgVola = abs(pixDetected-self.avgPix)*alpha + self.avgVola*(1-alpha)
self.avgPix = pixDetected*alpha + self.avgPix*(1-alpha)
return
# at this point we have a valid object detection
# cv2.imwrite('Videos/'+ts+'-1.jpg',mask)
# cv2.imwrite('Videos/'+ts+'-2.jpg',img)
# activation or prolongation of trigger signal when new object is detected
if not self.timestamp:
self.timestamp = ts
print('Object-Detection-Trigger active')
self.lastTrigger = now
return
def triggered(self):
return (self.timestamp)
class triggerGenerator
Den Triggergenerator gab es bisher auch schon. Das ist der Programmteil, der die Triggerdatei schreibt und damit die Videoaufzeichnung ansteuert. Im Unterschied zur vorhergehenden Version gibt es nun zwei Signalquellen (PIR und Bildauswertung), die von diesem Programmteil kombiniert werden müssen.
# Combines PIR- and ObjectDetection-Trigger and generates trigger signal for the camera
class triggerGenerator:
def __init__(self):
self.triggered = False # Trigger currently active?
self.lastTrigger = datetime.datetime.now() # stores last time of trigger detection
def trigger(self, ObjDet, Pir):
now = datetime.datetime.now()
if self.triggered:
# Trigger is already active
if ObjDet or Pir:
# One or both trigger (imageAnalyzer and PIR) still high
# prolongation of timeout
self.lastTrigger = datetime.datetime.now()
else:
# Both trigger (imageAnalyzer and PIR) are low
# Remove trigger signal (zero length file)
if (now - self.lastTrigger).total_seconds() > triggerTimeout:
triggerFiles = [f for f in os.listdir(triggerPath) if f.endswith(triggerFileExt)]
for f in triggerFiles:
os.remove(triggerPath+f)
self.triggered = False
print('Video stopped')
else:
# Trigger is inactive
if ObjDet and Pir:
# Both trigger (imageAnalyzer and PIR) high
# Set trigger signal (zero length file) and use the earlier timestamp
open(triggerPath + min(ObjDet, Pir) + triggerFileExt, 'w').close()
self.triggered = True
self.lastTrigger = datetime.datetime.now()
print('Video started')
Die Methode trigger
bekommt mit ObjDet
und Pir
jeweils den Status der beiden Detektoren übergeben. Das ist ein Timestamp, wenn aktuell eine Erkennung stattfindet oder andernfalls ein leerer String. Dann wird in einer Kaskade von If-Ausscheidungen ermittelt, ob eine Triggerdatei geschrieben oder gelöscht werden muss. Oder ob im Rahmen der Nachlaufzeit gewartet wird, ob evtl. noch einmal ein Erkennungssignal zurück kommt. Zur Erinnerung, die Nachlaufzeit ist die Zeitspanne, während der das Video nach Verschwinden des Eichhörnchens noch weiter läuft.
Gesamtprogramm (analyze2.py
)
Und hier der Code für das gesamte Programm zum einfachen Rauskopieren:
import io
import os
import time
import datetime
import cv2
import numpy as np
import RPi.GPIO as GPIO
imagePath = "/tmp/" # path to image and timestamp files
tsExt = ".sig" # file extension of timestamp file
imageFile = "record.jpg" # name of image file
alpha = 0.1 # amount of influence of a single value to the computed average
sigma = 7.0 # value to multipy volatility with for a higher threshold
minPix = 250 # minimum pixel to detect 275
sunset = 60 # lower brightnes switches to night
sunrise = 65 # higher brightness switches to day
objectDetTriggerTO = 10 # timeout for object detection trigger
triggerTimeout = 20 # min. trigger duration, time extends, when triggered again
triggerFileExt = '.trg' # file extension for trigger signal file
triggerPath = 'trigger/' # path for trigger signal file
GPIO_PIR = 12 # used pin for PIR motion detector
class pir:
def __init__(self, pin):
self.timestamp = ''
GPIO.setmode(GPIO.BOARD)
# Set pin as input
GPIO.setup(pin,GPIO.IN)
# Bind event to callbac function
GPIO.add_event_detect(pin, GPIO.BOTH, self.PIR_callback)
def PIR_callback(self, pin):
triggertime = datetime.datetime.now()
if GPIO.input(pin) == 1:
self.timestamp = triggertime.strftime("%Y-%m-%d-%H-%M-%S")
print('PIR-Trigger active')
else:
print('PIR-Trigger cleared')
self.timestamp = ''
def triggered(self):
return (self.timestamp)
class imageAnalyzer:
def __init__(self):
self.avgPix = -1 # average number of detected pixel
self.avgVola = -1 # average volatility (gap between number of detected pixel and their average value)
self.night = False # indicator for day or night
self.timestamp = '' # indicates current active object detection
self.lastTrigger = datetime.datetime.now() # stores last time of object detection
def detect(self, img, ts):
now = datetime.datetime.now()
# Detect whether day or night
# Convert to grayscale
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# Brightness is the average of all pixels
brightness = np.average(gray)
# Change from night to day or vice versa
if self.night and brightness > sunrise: self.night = False
if not self.night and brightness < sunset: self.night = True
# Find matching pixel (for red and brown squrrel) in image
# Convert BGR to HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# define range of red color in HSV
lower_value = np.array([0,70,110])
upper_value = np.array([11,170,220])
maskRed = cv2.inRange(hsv, lower_value, upper_value)
lower_value = np.array([165,70,110])
upper_value = np.array([179,170,220])
maskRed2 = cv2.inRange(hsv, lower_value, upper_value)
# define range of brown color in HSV
lower_value = np.array([12,40,220])
upper_value = np.array([17,65,255])
maskBrown = cv2.inRange(hsv, lower_value, upper_value)
lower_value = np.array([125,30,45])
upper_value = np.array([170,65,110])
maskBrown2 = cv2.inRange(hsv, lower_value, upper_value)
# Mask for red and brown
mask = maskRed+maskRed2+maskBrown+maskBrown2
pixDetected = cv2.countNonZero(mask)
# special case: average values not yet computed
if self.avgPix == -1:
self.avgPix = pixDetected
self.avgVola = minPix * sigma
# calc threshold (pixDetected must be higher for a valid object detection)
movingBand = self.avgVola * sigma
if movingBand < minPix:
movingBand = minPix
threshold = self.avgPix + movingBand
print("{0} Pix: {1:5d} Avg: {2:5.0f} Vola: {3:5.0f} Thresh: {4:5.0f} Bright: {5:3.0f} {6}{7}" \
.format(ts, pixDetected, self.avgPix, self.avgVola, threshold, brightness, \
"-" if self.night==True else "", \
"T" if pixDetected >= threshold else ""))
# cv2.imwrite('result.jpg',mask)
# cv2.imwrite('result1.jpg',img)
# no object detection because it is night or amount of detected pixels is below threshold
if self.night or pixDetected < threshold:
# clearing of trigger signal if timed out
if self.timestamp:
if (now - self.lastTrigger).total_seconds() > objectDetTriggerTO:
self.timestamp = ''
print('Object-Detection-Trigger cleared')
else:
# update average values
self.avgVola = abs(pixDetected-self.avgPix)*alpha + self.avgVola*(1-alpha)
self.avgPix = pixDetected*alpha + self.avgPix*(1-alpha)
return
# at this point we have a valid object detection
# cv2.imwrite('Videos/'+ts+'-1.jpg',mask)
# cv2.imwrite('Videos/'+ts+'-2.jpg',img)
# activation or prolongation of trigger signal when new object is detected
if not self.timestamp:
self.timestamp = ts
print('Object-Detection-Trigger active')
self.lastTrigger = now
return
def triggered(self):
return (self.timestamp)
# Combines PIR- and ObjectDetection-Trigger and generates trigger signal for the camera
class triggerGenerator:
def __init__(self):
self.triggered = False # Trigger currently active?
self.lastTrigger = datetime.datetime.now() # stores last time of trigger detection
def trigger(self, ObjDet, Pir):
now = datetime.datetime.now()
if self.triggered:
# Trigger is already active
if ObjDet or Pir:
# One or both trigger (imageAnalyzer and PIR) still high
# prolongation of timeout
self.lastTrigger = datetime.datetime.now()
else:
# Both trigger (imageAnalyzer and PIR) are low
# Remove trigger signal (zero length file)
if (now - self.lastTrigger).total_seconds() > triggerTimeout:
triggerFiles = [f for f in os.listdir(triggerPath) if f.endswith(triggerFileExt)]
for f in triggerFiles:
os.remove(triggerPath+f)
self.triggered = False
print('Video stopped')
else:
# Trigger is inactive
if ObjDet and Pir:
# Both trigger (imageAnalyzer and PIR) high
# Set trigger signal (zero length file) and use the earlier timestamp
open(triggerPath + min(ObjDet, Pir) + triggerFileExt, 'w').close()
self.triggered = True
self.lastTrigger = datetime.datetime.now()
print('Video started')
pi = pir(GPIO_PIR)
il = imageLoader()
ia = imageAnalyzer()
tg = triggerGenerator()
while True:
timeStamp, img = il.getImg()
ia.detect(img, timeStamp)
tg.trigger(ia.triggered(), pi.triggered())
Wenn diese Programmversion produktiv gesetzt und automatisch gestartet werden soll, muss natürlich die Datei /etc/rc.local
wie folgt angepasst werden:
# Autostart RaspiCam
cd /home/pi
rm -f trigger/*
su pi -c 'python3 -u record2.py &> record.log &'
su pi -c 'python3 -u analyze2.py &> analyze.log &'
exit 0
Erfahrungen
In der Tat verbessert die Kombination aus Farberkennung und Bewegungssensor die Treffsicherheit des Kameraauslösers signifikant. Die Anzahl der falsch positiven Erkennungen geht zurück und vorzeitige Kameraabschaltungen bei bewegungslosem Eichhörnchen ebenfalls. Schwierig bleibt es aber für den Kameratrigger, wenn Wind (also bewegte Äste) und Gegenlichtsituationen zeitlich zusammenfallen.
Weitere Artikel in dieser Kategorie:
- Raspberry Video Camera – Teil 1: Oachkatzl-Cam
- Raspberry Video Camera – Teil 2: Komponenten & Konzepte
- Raspberry Video Camera – Teil 3: Raspberry Pi Kamera Modul V2.1
- Raspberry Video Camera – Teil 4: Aufnahmeauslöser
- Raspberry Video Camera – Teil 5: Passiver Infrarot Bewegungssensor
- Raspberry Video Camera – Teil 6: Stromversorgung
- Raspberry Video Camera – Teil 7: Spannungsregler 5V
- Raspberry Video Camera – Teil 8: Montage Modell 850
- Raspberry Video Camera – Teil 9: Montage Kamera Modul
- Raspberry Video Camera – Teil 10: SW Installation Betriebssystem und Module
- Raspberry Video Camera – Teil 11: SW Python für die Kamera
- Raspberry Video Camera – Teil 12: SW Trigger per Bewegungssensor
- Raspberry Video Camera – Teil 13: SW Autostart und Überwachung
- Raspberry Video Camera – Teil 14: SW Installation Computer Vision (OpenCV 3.2)
- Raspberry Video Camera – Teil 15: SW Einzelbilder exportieren für die Farberkennung
- Raspberry Video Camera – Teil 16: SW Trigger per Farberkennung
- Raspberry Video Camera – Teil 17: Exkurs – Wie Computer Farben sehen
- Raspberry Video Camera – Teil 18: SW Farbkalibrierung
- Raspberry Video Camera – Teil 20: Exkurs – Farbdarstellung per 2D-Histogramm
- Raspberry Video Camera – Teil 21: Konzept einer selbstlernenden Farberkennung
- Raspberry Video Camera – Teil 22: SW selbstlernende Farberkennung in Python
- Raspberry Video Camera – Teil 23: Verbesserung durch ROI und Aufnahmezeitbegrenzung
- Raspberry Video Camera – Teil 24: Anpassung von Programmparametern
- Raspberry Video Camera – Teil 25: Zweite Kamera
- Raspberry Video Camera – Teil 26: Optimierungen gegen Frame Drops
- Raspberry Video Camera – Teil 27: Was kostet der Spaß?
- Raspberry Video Camera – Teil 28: Kamera Modell 200 mit Raspberry Pi Zero
- Raspberry Video Camera – Teil 29: Stromversorgung für Kamera Modell 200
- Raspberry Video Camera – Teil 30: Software für Kamera Modell 200