Konzeptionell habe ich die selbstlernende Farberkennung zur Kameraauslösung im letzten Artikel bereits besprochen. Nun geht es um die Realisierung in Form eines Python-Programms für den Raspberry Pi. Dabei baue ich auf dem zuletzt besprochenen Kombinationstrigger-Programm auf und erweitere es um ein paar zusätzliche Komponenten. Damit ist es künftig nicht mehr nötig, Farbbereiche für die Erkennung von Hand zu definieren. Das Programm wird die Farben selbständig anhand von Beispielbildern lernen und diese dann verwenden um die Kamera zu triggern.
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.
Was wird benötigt?
Mit über 20 Artikeln ist diese Serie inzwischen sehr üppig geworden und vielleicht auch ein wenig unübersichtlich. Deshalb möchte ich an dieser Stelle kurz auflisten, was denn alles benötigt wird, um das nachfolgend beschriebene Programm einsetzen zu können. Und dazu gibt es jeweils den Link zum entsprechenden Artikel.
- Als Hardware eine Raspberry Pi Video Kamera Modell 850B mit Bewegungssensor
- Raspbian Betriebssystem und ein paar Softwaremodule
- OpenCV als Computer Vision Software
- Optional Matplotlib zum Plotten von Histogrammen
- Die Pythonprogramme
record2.py
undpostprocess.py
im Homeverzeichnis des Users Pi - Die Unterverzeichnisse
Videos
,trigger
undsamples
ebenfalls im Homeverzeichnis von Pi - Freigestellte Beispielbilder im Verzeichnis
samples
, damit das Programm daraus die Farben der Objekte lernen kann
Andere Programme, die in dieser Artikelserie bereits vorgestellt wurden, wie record.py
, motioninterrupt.py
, analyze.py
oder analyze2.py
werden nicht mehr benötigt.
Darüber hinaus empfehle ich zur Lektüre meinen vorangegangenen Artikel, in dem es konzeptionell um die selbstlernende Farberkennung geht.
Python-Programm analyze3.py
Das Analyseprogramm kommt in die dritte Generation, nach Farberkennung und Kombinationstrigger kommt nun die selbstlernende Farberkennung, die auch als Kombinationstrigger ausgeführt ist. Kombinationstrigger bedeutet in diesem Zusammenhang, dass der PIR-Bewegungssensor zusätzlich zur Bildauswertung als zweiter Trigger mitgeführt wird, um die Auslösesignale qualitativ zu verbessern.
Unveränderte Programmteile
Die gute Nachricht ist, dass wir das Python-Programm nicht komplett umschreiben müssen. Einige Klassen schon und es kommen auch Programmteile dazu, aber die folgenden bleiben im Vergleich zu analyze2.py
unverändert:
class pir
class imageloader
undclass triggerGenerator
Neu ist vor allen Dingen die Lernfunktion, die aus Beispielbildern die typischen Farben der Zielobjekte (hier Eichhörnchen) selbständig ermitteln soll. Und damit geht es gleich los.
Funktion initialLearn
Die initiale Lernfunktion wird nur einmal beim Start des Programms ausgeführt, deshalb mache ich gar kein Objekt daraus, sondern belasse es bei einer einfachen Funktion, die ich hier in Einzelteilen vorstelle. Das gesamte Programm – auch mit den unveränderten Teilen – steht dann nochmal komplett am Ende dieses Artikels, damit es sich einfach am Stück kopieren lässt.
# Initial learning function
# Reads sample images and calculates combined histograms
def initialLearn():
print('Initial learning from sample images')
histos = {}
connections = []
print('Reading sample images from folder: '+samplePath)
# Read sample images and calc histograms
for f in os.listdir(samplePath): # all files in directory
if not os.path.isfile(os.path.join(samplePath, f)): # skip directories
continue
name = os.path.splitext(f)[0] # get name without extension
img = cv2.imread(samplePath+f,1) # load image
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # convert to HSV
hist = cv2.calcHist([hsv],[0,1],None,[hueBins,satBins],[0,180, 0,256]) # calc histogram satu + hue
hist[0,0]=0 # delete background color (black))
cv2.normalize(hist,hist,0,100,cv2.NORM_MINMAX) # normalize values to 100
Zu Beginn der Lernfunktion definiere ich zwei Variablen, ein Dictionary histos
, das die Histogramme aufnehmen wird und eine Liste connections
. Letztere brauchen wir, um die Ähnlichkeiten der einzelnen Histogramme zu einander ermitteln zu können, damit sich ähnliche Histogramme zusammenführen lassen.
Dann werden der Reihe nach alle Bilder, die sich im Verzeichnis samplePath
befinden, eingelesen. Aus dem Dateinamen ohne Erweiterung ergibt sich der Name des Bilds, bzw seines Histogramms. Das Bild selbst wird in den HSV-Farbraum überführt und dann wird aus ihm ein zweidimensionales Histogramm aus Hue (Farbwert) und Saturation (Sättigung) erzeugt. Eine Farbe muss aus dem Histogramm gelöscht werden, das ist der Hintergrund des freigestellten Objekts, in diesem Fall schwarz [0,0]. Und zum Schluss erfolgt noch eine Normalisierung des Histogramms. Das bewirkt, dass der größte Histogrammwert (also die Pixelanzahl der Farbe, die am häufigsten vorkommt) auf den Wert 100 justiert wird und alle kleineren Werte proportional dazu. Damit liegen alle Histogramme wertmäßig zwischen 0 und 100 und können so gut verglichen werden.
# Build a connection list from distances between each sample
# Distance is a measure of the similarity of two histograms computed from histogram comparison
# with hellinger method: 0 = equal and 1 = total different
if len(histos) > 0:
for hi in histos: # with each already existing histogram
diff = cv2.compareHist(hist,histos[hi],cv2.HISTCMP_HELLINGER) # compare two histograms
connections.append([diff,name,hi]) # and store [diff, partner a, partner b]
# in connection list
histos[name] = hist # finally store histogram (name is key)
print('{0} sample images loaded - now combining similar images ...\n'.format(len(histos)))
# Sort list of connections (nearest first)
connections.sort(key=lambda c: c[0]) # sort on distance
Im zweiten Teil befinden wir uns zunächst immer noch in der großen For-Schleife, in der die Bilder eingelesen werden. Nun wird aus den Histogrammen eine „Verbindungsliste“ aufgebaut, in der von jedem Histogramm zu jedem anderen eine Verbindung hergestellt wird. Der Wert dieser Verbindung ist eine Distanz und die wiederum ein Maß für die Ähnlichkeit von zwei Histogrammen. Der Vorgang beginnt, sobald wir mindestens ein Histogramm bereits gespeichert haben, mit dem wir das aktuelle vergleichen können.
Der Vergleich erfolgt mit cv2.compareHist
nach der Hellinger-Methode. Die ergibt beim Vergleich von zwei Histogrammen den Wert 0, wenn beide Histogramme absolut gleich sind und den Wert 1, wenn sie total verschieden sind. Diesen Unterschiedswert diff
speichern wir zusammen mit den Namen beider Histogramme in der Verbindungsliste connections
. Und das Histogramm selbst wird schließlich mit seinem Namen ins Directory histos
eingetragen.
Am Ende der großen Einleseschleife (also beim print
Befehl) haben wir ein Python-Directory aller Histogramme zur Verfügung und eine Verbindungsliste mit allen möglichen Paarbeziehungen zwischen jeweils zwei Histogrammen und deren Ähnlichkeitswert. Und nach letzterem wird die Verbindungsliste abschließend sortiert, so dass die ähnlichsten Paarbeziehungen am Anfang der Liste stehen und die unterschiedlicheren am Ende.
# combine most similar histograms to get a shorter histogram list
while connections[0][0] < simili and len(histos) > 2: # while small distances
a = connections[0][1] # partner a
b = connections[0][2] # partner b
print("Similarity: {0:1.3f} combine {1} with {2}" \
.format(connections[0][0],a,b))
connections[:] = [x for x in connections if x[1]!=a and x[1]!=b and \
x[2]!=a and x[2]!=b] # clear a and b from list
# hist = histos[a] + histos[b] # add histograms
hist = np.maximum(histos[a], histos[b]) # combine histogram max values
name = a + b # combine names
del histos[a] # delete a and b histograms
del histos[b]
for hi in histos: # calc new connections
diff = cv2.compareHist(hist,histos[hi],cv2.HISTCMP_HELLINGER)
connections.append([diff,name,hi])
histos[name] = hist # store combined histogram
connections.sort(key=lambda c: c[0]) # sort again on distance
print('\nCombined to {0} detector histograms\n'.format(len(histos)))
# Normalize all remaining histograms
for h in histos:
cv2.normalize(histos[h],histos[h],0,100,cv2.NORM_MINMAX) # adjust values between 0 and 100
return(histos)
Im dritten Teil der Funktion geht es nun darum, die vielen Histogrammen zu einigen wenigen zu verdichten. Dazu wählen wir die beiden Histogramme aus, die sich am ähnlichsten sind. Denn wir können annehmen, dass sie zu dem selben Objekt gehören (rotbraunes oder dunkelbraunes Eichhörnchen). Das ist jetzt ganz leicht, dann die beiden ähnlichsten Histogramme stehen in der Verbindungsliste ganz am Anfang. Die beiden Histogramme (a und b) müssen nun vereinigt werden. Das könnte durch simple Numpy-Addition erfolgen, wie in der auskommentierten Zeile angedeutet. Besser ist aber die Variante, mit np.maximum
jeweils den höheren Wert in das resultierende Histogramm zu übernehmen.
Vereinigt werden auch die Namen der beiden Histogramme um dem resultierenden Histogramm auch einen Namen zu geben. Zur Erinnerung, der Histogrammname ist der Dateiname des Beispielbilds ohne Dateierweiterung. Und der wurde hoffentlich recht kurz gewählt, denn der Name des neuen Histogramms entsteht, in dem die beiden Einzelnamen von a und b aneinander gehängt werden. (Und in Folge so fort, so dass lange Namen entstehen können.)
Nun müssen noch die beiden ursprünglichen Histogramme a und b aus dem Directory histos
gelöscht werden und ebenso alle Paarbeziehungen an denen a oder b beteiligt sind. Dann wird das verdichtete Histogramm in histos
aufgenommen und natürlich neue Paarbeziehungen anhand der Ähnlichkeit des neuen Histogramms zu allen anderen berechnet und in der Verbindungsliste gespeichert. Abschließend wird die Verbindungsliste wieder nach Ähnlichkeit sortiert.
Das ganze erfolgt in einer While-Schleife so lange, bis ein gewünschter Grad an Unterschiedlichkeit der verbliebenen Histogramme erreicht ist. Oder, um es anders auszudrücken, es wird so lange verdichtet, bis alle ähnlichen Histogramme zusammengefasst sind. Das Maß für die gewünschte Unterschiedlichkeit ist die Variable simili
, die hier mit 0.5 vorbelegt ist. 0.5 deshalb, weil es die Mitte zwischen 0 (gleich) und 1 (unterschiedlich) darstellt. Aber hier kann natürlich experimentiert werden.
Am Ende dieser Funktion haben wir aus vielen Histogrammen einige wenige erhalten, die mit return
zurückgegeben werden.
class imageAnalyzer
Die Klasse imageAnalyzer
gab es bisher auch schon. Sie hatte für jedes Bild, das sekündlich von der Kamera bereitgestellt wird, die Farbanalyse vorgenommen, um ein Eichhörnchen im Bild zu erkennen. Das bleibt an sich auch so, nur dass wir bisher nur einen Detektor hatten (wenn wir vom Hardware PIR-Sensor mal absehen) und nun haben wir mehrere. Nämlich genau einen für jedes Farbhistogramm, das am Ende von initialLearn
übrig geblieben ist. Der imageAnalyzer macht die Farberkennung deshalb nicht mehr selber, sondern steuert für jedes Bild der Reihe nach alle Detektoren an und nimmt deren Prüfergebnis entgegen.
Im Konstruktor __init__
bekommt er zu diesem Zweck eine Liste der Detektorobjekte übergeben und speichert sie in self.detectors
.
# Uses multiple detectors to find object colors in the given image
class imageAnalyzer:
def __init__(self, detectors):
self.detectors = detectors # list ofimage detector histograms
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
self.lastHistUpdate = datetime.datetime.now() - datetime.timedelta(hours = 1)
def detect(self, img, ts):
now = datetime.datetime.now()
# Detect whether day or night
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) # convert to grayscale
brightness = np.average(gray) # calc average of all pixels
# 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
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # convert BGR to HSV
imgDetected = False
for d in self.detectors: # loop over detector objects
imgDetected = d.detect(hsv, ts) or imgDetected
print()
# no object detection because it is night or amount of detected pixels is below threshold
if self.night or not imgDetected:
# clearing of trigger signal if timed out
if self.timestamp:
if (now - self.lastTrigger).total_seconds() > objectDetTriggerTO:
self.timestamp = ''
print('Object-Detection-Trigger cleared')
elif not self.night: # day, but no current and no previous trigger signal
if now - self.lastHistUpdate > histLifeTime: # histograms timed out
negativeHist = cv2.calcHist([hsv],[0,1],None,[hueBins,satBins],[0,180,0,256]) # calc histogram satu + hue
cv2.normalize(negativeHist,negativeHist,0,2000,cv2.NORM_MINMAX) # normalize to 2000
for d in self.detectors: # loop over detector objects
d.updateHist(negativeHist)
self.lastHistUpdate = now
return
# at this point we have a valid object detection
# 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)
Die Methode detect
wird dann mit jedem neuen Bild aufgerufen. Darin erfolgt zuerst die bereits bekannte Tag-Nacht-Erkennung, die verhindert, dass bei zu geringem Tageslicht die Kamera ausgelöst wird. Dann wird das Bild nach HSV überführt und in der nachfolgenden For-Schleife werden die einzelnen Detektoren aufgerufen:
for d in self.detectors:
imgDetected = d.detect(hsv, ts) or imgDetected
Die Variable imgDetected
, die anfänglich mit False
vorbesetzt ist, wird dabei mit jedem Rückgabewert eines Detektors oder-verknüpft, so dass sie am Ende in Summe für Erkennung oder Nichterkennung steht. In den nachfolgenden If-Ausscheidungen wird dann anhand von imgDetected
und der Nacht-Erkennung festgestellt, ob ein gültiger Farberkennungstrigger anliegt, ob er ggf. verlängert oder gelöscht werden muss. Das kennen wir im Prinzip alles schon von der vorhergehenden Version.
Neu und interessant ist der Fall, dass Tageslicht herrscht, aber aktuell keine Triggersituation vorliegt. Wenn die Histogram-Lebensdauer (15min) abgelaufen ist, wird das Kamerabild, das nun lediglich den aktuellen Hintergrund darstellt, genommen und daraus ein Negativ-Histogramm gebildet. „Negativ“ heißt es deshalb, weil es genau die Farben beinhaltet, auf die es uns nicht ankommt, nämlich die Hintergrundfarben. Im Gegensatz dazu steht das Positiv-Histogramm für die Eichhörnchenfarben. Das Negativ-Histogramm wird auf einen sehr hohen Maximalwert (20x höher als sonst) normalisiert, um auch wenig vertretenen Hintergrundfarben ein gewisses Gewicht zu geben. Und dann wird das Histogramm an alle Detektoren übergeben, die mit diesen Negativwerten ihre Farberkennung optimieren können.
class detector
Diese neue Klasse bildet nun die eigentliche Farberkennung ab und zur Laufzeit wird davon für jedes Detektor-Histogramm ein Objekt erzeugt. Im Konstruktor wird das Histogramm und dessen Name übergeben und beides gespeichert. Das Histogramm wird auch gleich noch kopiert, so dass das Objekt zwei Histogramme beinhaltet.self.positiveHist
ist das originale Histogramm, das aus einem oder mehreren Beispielbildern erzeugt wurde. Das wird während der Programmlaufzeit periodisch um die Farben des Hintergrunds bereinigt und das Ergebnis dann als self.hist
geführt. Initial sind beide Histogramme erst mal gleich.
# Single detector (holds one histogram)
class detector:
def __init__(self, name, hist):
self.name = name # histogram name
self.positiveHist = hist # histogram of sample positive images
self.hist = np.copy(self.positiveHist) # histogram to work with (initially copy of above)
self.avgPix = -1 # average number of detected pixel
self.avgVola = -1 # average volatility (gap between number of
# detected pixel and their average value)
# search for histogram colors in given hsv image
def detect(self, hsv, ts):
backPro = cv2.calcBackProject([hsv],[0,1],self.hist,[0,180,0,256],1) # histogram back projection
ret, mask = cv2.threshold(backPro, bProThresh,255,cv2.THRESH_BINARY) # delete pixel with too low value
pixDetected = cv2.countNonZero(mask) # count detected pixel
# special case: average values not yet computed or resetted due to histogram update
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} ({2:3.0f}% of Thresh) {3} {4}" \
.format(ts, pixDetected, pixDetected/threshold*100, \
"T" if pixDetected >= threshold else " ", self.name))
# no object detection because amount of detected pixels is below threshold
if pixDetected < threshold:
# update average values
self.avgVola = abs(pixDetected-self.avgPix)*alpha + self.avgVola*(1-alpha)
self.avgPix = pixDetected*alpha + self.avgPix*(1-alpha)
return(False)
else:
# at this point we have a valid object detection
return(True)
# Build new detection histogram considering changed background colors
def updateHist(self, negativeHist):
self.hist = self.positiveHist - negativeHist # elininate background colors
self.hist[self.hist<0] = 0 # set all negative values to zero
cv2.normalize(self.hist,self.hist,0,100,cv2.NORM_MINMAX) # and normalize
self.avgPix = -1 # reset average values
print('Detector histogram updated', self.name)
Die Methode detect
bekommt das aktuelle HSV-Image von der Kamera übergeben und geht gleich zur Sache. cv2.calcBackProject
führt die Histogramm-Rückprojektion durch, dabei wird quasi das Detektor-Histogramm mit den Eichhörnchenfarben auf das aktuelle Bild angewendet. Das Ergebnis ist ein Graustufenbild (backpro
), bei dem die Helligkeit eines Pixels als Erkennungsgrad für die Eichhörnchenfarben verstanden werden kann. Das Graustufenbild wird nun mittels eines Schwellwerts (bProThresh
) in ein Schwarzweißbild überführt. Dabei werden dunkle Pixel (mit niedrigen Werten unter dem Schwellwert) zu schwarz (0) und helle Pixel zu weiß (255). Das dient nur zur Beseitigung von unterschwelligen Gekräusel (also eher zufälligen niedrigen Werten). Und schließlich werden die weißen Pixel gezählt.- je mehr erkannte Pixel, desto mehr Eichhörnchen – könnte man sagen.
Der Rest funktioniert wie gehabt: es wird über die Zeit ein gleitender Durchschnitt gebildet, die Volatilität bestimmt, also die Schwankungsbreite der jeweils erkannten Pixel um den Durchschnittswert und ein Vielfaches der Volatilität über dem gleitenden Durchschnitt stellt dann die Erkennungsschwelle dar. Liegt die aktuelle Anzahl der erkannten Pixel über dieser Schwelle, gilt das Eichhörnchen (oder was auch immer) als erkannt. Durch diesen etwas umständlichen Mechanismus, kann sich das Programm ein wenig an langsam wechselnde Lichtsituationen anpassen.
Die Methode updateHist wird nur alle 15 Minuten aufgerufen und bekommt ein Negativhistogramm übergeben. Wie wir oben beim imageAnalyzer
bereits gesehen haben, ist das Negativhistogramm ein Ausdruck der aktuellen Hintergrundfarben, der zahlenmäßig auch noch auf das 20-fache aufgeblasen wurde. Dieses Negativhistogramm wird nun vom ursprünglichen Positivhistogramm subtrahiert. Das das Negativhistogramm zahlenmäßig überhöht ist, gehen durch sie Subtraktion nahezu alle Farben verloren, die im Hintergrund vertreten sind. Übrig bleiben nur die Eichhörnchenfarben, die sich vom Hintergrund unterscheiden. (Siehe Bild.)
Die Subtraktion führt natürlich im Histogramm zu vielen negativen Werten, die erst mal auf Null gezogen werden müssen. Dann erfolgt die übliche Normalisierung auf 100. Damit ist self.hist
aktualisiert. Damit in diesem Moment keine falsche Objekterkennung durch das schlagartig geänderte Histogramm erfolgt, wird self.avgPix
auf -1 gesetzt. Das bewirkt, dass die Durchschnittewerte neu angeglichen werden müssen und damit auch für ein paar Sekunden zu einer gewissen Blindheit der Farberkennung.
Hauptprogramm
So weit die neuen und geänderten Klassen und nun zum Hauptprogramm, das sich auch verändert hat:
# Main program
histos = initialLearn() # read sample images and learn their colors
detectors = []
for h in sorted(histos): # create detector objects from all histograms
detectors.append(detector(h, histos[h])) # and store them in a list
pi = pir(GPIO_PIR) # object for infrared motion sensor
il = imageLoader() # object to load image
ia = imageAnalyzer(detectors) # object to analyze image
tg = triggerGenerator() # object to create camera trigger file
while True: # endless loop
timeStamp, img = il.getImg() # wait for next image and load it
ia.detect(img, timeStamp) # detect colors
tg.trigger(ia.triggered(), pi.triggered()) # and create camera trigger file
Das Programm beginnt mit der initialen Lernfunktion. Deren Ergebnis ist ein Directory (histos
) aus Histogrammen. Daraus werden sortiert Detektorobjekte instanziiert und in der Liste detectors
gespeichert. Dann werden auch aus den übrigen Klassen Objekte abgeleitet – das kennen wir bereits. Und schließlich kommt die große Hauptschleife, die ewig durchlaufen wird.
Darin wird zuerst auf ein neues Bild von der Kamera gewartet, das wird dem imageAnalyzer
zugeführt, der seinerseits die einzelnen Detektoren aufruft und schließlich kümmert sich der triggerGenerator
um das Setzen und Löschen des Kameraauslösers.
Gesamtprogramm analyze3.py
Hier nun das ganze Programm an einem Stück – auch mit den Teilen, die sich nicht geändert haben:
import io
import os
import time
import datetime
import cv2
import numpy as np
import RPi.GPIO as GPIO
samplePath = "samples/" # path to sample images
hueBins = 30 # histogram bins for hue
satBins = 30 # histogram bins for saturation
simili = 0.5 # similarity limit, smaller values are considered similar
# greater values different
imagePath = "/tmp/" # path to image and timestamp files
tsExt = ".sig" # file extension of timestamp file
imageFile = "record.jpg" # name of image file
alpha = 0.075 # amount of influence of a single value to the computed average
sigma = 9.0 # value to multipy volatility with for a higher threshold
minPix = 275 # minimum pixel to detect
sunset = 60 # lower brightnes switches to night
sunrise = 65 # higher brightness switches to day
bProThresh = 50 # back projection threshold (0-255), lower values will be ignored
histLifeTime = datetime.timedelta(minutes = 15) # histogram lifetime, recalc if timed out
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
# Initial learning function
# Reads sample images and calculates combined histograms
def initialLearn():
print('Initial learning from sample images')
histos = {}
connections = []
print('Reading sample images from folder: '+samplePath)
# Read sample images and calc histograms
for f in os.listdir(samplePath): # all files in directory
if not os.path.isfile(os.path.join(samplePath, f)): # skip directories
continue
name = os.path.splitext(f)[0] # get name without extension
img = cv2.imread(samplePath+f,1) # load image
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # convert to HSV
hist = cv2.calcHist([hsv],[0,1],None,[hueBins,satBins],[0,180, 0,256]) # calc histogram satu + hue
hist[0,0]=0 # delete background color (black))
cv2.normalize(hist,hist,0,100,cv2.NORM_MINMAX) # normalize values to 100
# Build a connection list from distances between each sample
# Distance is a measure of the similarity of two histograms computed from histogram comparison
# with hellinger method: 0 = equal and 1 = total different
if len(histos) > 0:
for hi in histos: # with each already existing histogram
diff = cv2.compareHist(hist,histos[hi],cv2.HISTCMP_HELLINGER) # compare two histograms
connections.append([diff,name,hi]) # and store [diff, partner a, partner b]
# in connection list
histos[name] = hist # finally store histogram (name is key)
print('{0} sample images loaded - now combining similar images ...\n'.format(len(histos)))
# Sort list of connections (nearest first)
connections.sort(key=lambda c: c[0]) # sort on distance
# combine most similar histograms to get a shorter histogram list
while connections[0][0] < simili and len(histos) > 2: # while small distances
a = connections[0][1] # partner a
b = connections[0][2] # partner b
print("Similarity: {0:1.3f} combine {1} with {2}" \
.format(connections[0][0],a,b))
connections[:] = [x for x in connections if x[1]!=a and x[1]!=b and \
x[2]!=a and x[2]!=b] # clear a and b from list
# hist = histos[a] + histos[b] # add histograms
hist = np.maximum(histos[a], histos[b]) # combine histogram max values
name = a + b # combine names
del histos[a] # delete a and b histograms
del histos[b]
for hi in histos: # calc new connections
diff = cv2.compareHist(hist,histos[hi],cv2.HISTCMP_HELLINGER)
connections.append([diff,name,hi])
histos[name] = hist # store combined histogram
connections.sort(key=lambda c: c[0]) # sort again on distance
print('\nCombined to {0} detector histograms\n'.format(len(histos)))
# Normalize all remaining histograms
for h in histos:
cv2.normalize(histos[h],histos[h],0,100,cv2.NORM_MINMAX) # adjust values between 0 and 100
return(histos)
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 imageLoader:
def __init__(self):
self.timeout = datetime.timedelta(0, 1) # default 1sec
self.lastTsTime = datetime.datetime.now() - self.timeout # time of last timestamp
self.lastTs = "" # last timestamp as a string
def readTimeStamp(self):
# Read names of all timestamp files, take the first (should be only one) and isolate timestamp
tsFiles = [f for f in os.listdir(imagePath) if f.endswith(tsExt)]
if len(tsFiles) > 0:
return tsFiles[0][0:-4]
else:
return False
def getImg(self):
# sleep until next expected image file, wait for valid time stamp file and then read image file
# adjust sleep time until next image (+/- 0.01sec)
wTime = (self.timeout - (datetime.datetime.now() - self.lastTsTime)).total_seconds()
if wTime > 0:
print("\n",wTime)
time.sleep(wTime)
ts=self.readTimeStamp()
if not ts or (ts == self.lastTs):
self.timeout += datetime.timedelta(milliseconds=10)
time.sleep(0.01)
ts=self.readTimeStamp()
while not ts or (ts == self.lastTs):
time.sleep(0.01)
ts=self.readTimeStamp()
else:
self.timeout -= datetime.timedelta(milliseconds=10)
self.lastTsTime = datetime.datetime.now()
self.lastTs = ts
return (ts, cv2.imread('/tmp/record.jpg',1))
# Single detector (holds one histogram)
class detector:
def __init__(self, name, hist):
self.name = name # histogram name
self.positiveHist = hist # histogram of sample positive images
self.hist = np.copy(self.positiveHist) # histogram to work with (initially copy of above)
self.avgPix = -1 # average number of detected pixel
self.avgVola = -1 # average volatility (gap between number of
# detected pixel and their average value)
# search for histogram colors in given hsv image
def detect(self, hsv, ts):
backPro = cv2.calcBackProject([hsv],[0,1],self.hist,[0,180,0,256],1) # histogram back projection
ret, mask = cv2.threshold(backPro, bProThresh,255,cv2.THRESH_BINARY) # delete pixel with too low value
pixDetected = cv2.countNonZero(mask) # count detected pixel
# special case: average values not yet computed or resetted due to histogram update
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} ({2:3.0f}% of Thresh) {3} {4}" \
.format(ts, pixDetected, pixDetected/threshold*100, \
"T" if pixDetected >= threshold else " ", self.name))
# no object detection because amount of detected pixels is below threshold
if pixDetected < threshold:
# update average values
self.avgVola = abs(pixDetected-self.avgPix)*alpha + self.avgVola*(1-alpha)
self.avgPix = pixDetected*alpha + self.avgPix*(1-alpha)
return(False)
else:
# at this point we have a valid object detection
return(True)
# Build new detection histogram considering changed background colors
def updateHist(self, negativeHist):
self.hist = self.positiveHist - negativeHist # elininate background colors
self.hist[self.hist<0] = 0 # set all negative values to zero
cv2.normalize(self.hist,self.hist,0,100,cv2.NORM_MINMAX) # and normalize
self.avgPix = -1 # reset average values
print('Detector histogram updated', self.name)
# Uses multiple detectors to find object colors in the given image
class imageAnalyzer:
def __init__(self, detectors):
self.detectors = detectors # list ofimage detector histograms
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
self.lastHistUpdate = datetime.datetime.now() - datetime.timedelta(hours = 1)
def detect(self, img, ts):
now = datetime.datetime.now()
# Detect whether day or night
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) # convert to grayscale
brightness = np.average(gray) # calc average of all pixels
# 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
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # convert BGR to HSV
imgDetected = False
for d in self.detectors: # loop over detector objects
imgDetected = d.detect(hsv, ts) or imgDetected
print()
# no object detection because it is night or amount of detected pixels is below threshold
if self.night or not imgDetected:
# clearing of trigger signal if timed out
if self.timestamp:
if (now - self.lastTrigger).total_seconds() > objectDetTriggerTO:
self.timestamp = ''
print('Object-Detection-Trigger cleared')
elif not self.night: # day, but no current and no previous trigger signal
if now - self.lastHistUpdate > histLifeTime: # histograms timed out
negativeHist = cv2.calcHist([hsv],[0,1],None,[hueBins,satBins],[0,180,0,256]) # calc histogram satu + hue
cv2.normalize(negativeHist,negativeHist,0,2000,cv2.NORM_MINMAX) # normalize to 2000
for d in self.detectors: # loop over detector objects
d.updateHist(negativeHist)
self.lastHistUpdate = now
return
# at this point we have a valid object detection
# 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')
# Main program
histos = initialLearn() # read sample images and learn their colors
detectors = []
for h in sorted(histos): # create detector objects from all histograms
detectors.append(detector(h, histos[h])) # and store them in a list
pi = pir(GPIO_PIR) # object for infrared motion sensor
il = imageLoader() # object to load image
ia = imageAnalyzer(detectors) # object to analyze image
tg = triggerGenerator() # object to create camera trigger file
while True: # endless loop
timeStamp, img = il.getImg() # wait for next image and load it
ia.detect(img, timeStamp) # detect colors
tg.trigger(ia.triggered(), pi.triggered()) # and create camera trigger file
Automatisch starten
Für eigene Entwicklungen empfehle ich, das Modul Matplotlib einzubinden und die Histogramme, die das Programm generiert, in Dateien zu plotten. Nur so bekommt man ein Bild von dem, was im Programm wirklich vorgeht. Allerdings ist das relativ resourcenintensiv, so dass man im Produktivbetrieb darauf besser verzichten sollte.
Nachdem sich der Programmname geändert hat, muss für einen automatischen Programmstart die Datei /etc/rc.local
angepasst werden und zwar folgendermaßen:
# Autostart RaspiCam
cd /home/pi
rm -f trigger/*
su pi -c 'python3 -u record2.py &> record.log &'
su pi -c 'python3 -u analyze3.py &> analyze.log &'
exit 0
Auf das exit 0
am Ende achten, das muss stehen bleiben!
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 19: SW Kombinationstrigger
- Raspberry Video Camera – Teil 20: Exkurs – Farbdarstellung per 2D-Histogramm
- Raspberry Video Camera – Teil 21: Konzept einer selbstlernenden Farberkennung
- 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