Kundenzähler mittels optischer Erkennung (OpenCV)

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
Benutzeravatar
Trittbrettfahrer
User
Beiträge: 2
Registriert: Donnerstag 18. März 2021, 10:31
Wohnort: Köln

Guten Morgen liebe Community,

ich versuche gerade für die Filiale meines Vaters eine Art Personenzähler mittels eines Raspberry Pi und einer USB-Kamera zur realisieren. Rein zum Lernen, daher brauche ich auch keine anderen Vorschläge, wie Einkaufskörbe oder Bändchen um das max. betreten der Kunden zu überwachen.
Da ich noch nicht so gut im Python programmieren bin, habe ich mir aus "Modulen" eines bestehenden Programms ein wenig den Code abgeändert und nutze diesen.
Ziel des ganzen soll eine optische Erkennung der Kunden von oben sein.
Daraufhin wird diesen eine ID zugewiesen. Diese ID (Punkt) kann sich nun zwischen 2 Linien (programmiert) bewegen.
Wenn der Punkt von einer Seite kommend die zwei Linien durchbricht, wird entweder hoch oder runtergezählt.

Probleme habe ich mit der Stabilität des Programms und der Rechenleistung. Zumal das Programm momentan noch auf meinem Hauptrechner läuft und später mal auf dem Raspberry Pi laufen soll.
Falls mir jemand Tipps geben kann, was ich an dem Programm verändern soll, wäre ich ihm sehr dankbar. Mittels eines kleinen Testvideos hat es schonmal ganz gut funktioniert, jedoch ist bei einem livestream, die Datenmenge nicht mehr so ganz überschaubar. Evtl. muss ich da etwas an der FpS verstellen.
Ich packe den Code mal Hier rein, es sind 2 Dateien.
Datei 1 Person.py

Code: Alles auswählen

from random import randint
import time

class MyPerson:
    tracks = []
    def __init__(self, i, xi, yi, max_age):
        self.i = i
        self.x = xi
        self.y = yi
        self.tracks = []
        self.R = randint(0,255)
        self.G = randint(0,255)
        self.B = randint(0,255)
        self.done = False
        self.state = '0'
        self.age = 0
        self.max_age = max_age
        self.dir = None
        self.linija1 = None
        self.linija2 = None
        self.dingimas=0
    def getRGB(self):
        return (self.R,self.G,self.B)
    def getTracks(self):
        return self.tracks
    def getDingimas(self):
        return self.dingimas
    def updateDingimas(self, x):
        self.dingimas=x
    def getId(self):
        return self.i
    def getState(self):
        return self.state
    def getDir(self):
        return self.dir
    def getX(self):
        return self.x
    def getY(self):
        return self.y
    def updateCoords(self, xn, yn):
        self.age = 0
        self.tracks.append([self.x,self.y])
        self.x = xn
        self.y = yn
    def setDone(self):
        self.done = True
    def timedOut(self):
        return self.done
    def going_UP(self,mid_start,mid_end):
        if len(self.tracks) >= 2:
            if self.state == '0':
                if self.tracks[1][1] > mid_end and self.tracks[-2][1] <= mid_start: #Überqueren der Linie
                    self.state = '1'
                    self.dir = 'up'
                    return True
            else:
                return False
        else:
            return False
    def going_DOWN(self,mid_start,mid_end):
        if len(self.tracks) >= 2:
            if self.state == '0':
                if self.tracks[1][1] < mid_start and self.tracks[-2][1] >= mid_end: #Überqueren der Linie
                    self.state = '1'
                    self.dir = 'down'
                    return True
            else:
                return False
        else:
            return False

    def cross_bottom(self, bottom_line):
        if self.tracks[0][1] < bottom_line < self.tracks[-1][1]: #Die Linie von unten überqueren
            if self.linija1 != 'top':
                self.linija1 = 'bottom'
            else: self.linija2 = 'bottom'
        if self.tracks[0][1] > bottom_line > self.tracks[-1][1]: #gekreuzte Linie ist vorbei
            if self.linija1!='top':
                self.linija1 = 'bottom'
            else: self.linija2 = 'bottom'
                

    def cross_top(self, top_line):
        if self.tracks[0][1] < top_line < self.tracks[-1][1]: #Die Linie von oben überqueren
            if self.linija1!='bottom':
                self.linija1 = 'top'
            else: self.linija2 = 'top'
        if self.tracks[0][1] > top_line > self.tracks[-1][1]: #Die Linie von oben überqueren
            if self.linija1!='bottom':
                self.linija1 = 'top'
            else: self.linija2 = 'top'        
            
    def kurEina(self,bottom_line,top_line):
        if len(self.tracks) >= 2:
            if self.dir == None:
                self.cross_bottom(bottom_line)
                self.cross_top(top_line)
                if  self.linija1== 'top' and self.linija2== 'bottom':
                    self.dir = 'down'
                elif  self.linija1== 'bottom' and self.linija2== 'top':
                    self.dir = 'up'
        else:
            return False
        
    def age_one(self):
        self.age += 1
        if self.age > self.max_age:
            self.done = True
        return True
    
class MultiPerson:
    def __init__(self, persons, xi, yi):
        self.persons = persons
        self.x = xi
        self.y = yi
        self.tracks = []
        self.R = randint(0,255)
        self.G = randint(0,255)
        self.B = randint(0,255)
        self.done = False
        
Datei 2 track_people.py

Code: Alles auswählen

import numpy as np
import cv2
import Person
import time
import imutils
import datetime


cap = cv2.VideoCapture("Ressources/Pedestrian_detector.mp4") #Webcam als Quelle nutzen
fgbg = cv2.createBackgroundSubtractorMOG2(detectShadows = True) #Hintergrundsubrathierer

kernelOp = np.ones((3,3),np.uint8)
kernelOp1 = np.ones((7,7),np.uint8)
kernelOp2 = np.ones((5,5),np.uint8)

kernelCl = np.ones((11,11),np.uint8)
kernelCl1 = np.ones((20,20),np.uint8)
kernelCl2 = np.ones((25,25),np.uint8)

#Variabeln
font = cv2.FONT_HERSHEY_SIMPLEX
persons = []
max_p_age = 5
pid = 1
areaTH = 5000
w_margin= 50
h_margin= 50
wmax= 500


#import pdb; pdb.set_trace() #Keine Ahnung warum hier der Debugger ist.

# Darstellungsvariabeln
cnt_up=0
cnt_down=0
line_down_color=(255,0,0)
line_up_color=(0,0,255)
pts_L1= np.array([[0, 320],[480, 320]])
pts_L2= np.array([[0, 400],[480, 400]])


counter=0



while(cap.isOpened()):
    ret, frame = cap.read() #auslesen eines Frames

    frame = imutils.resize(frame, width=min(640, frame.shape[1]))

    fgmask = fgbg.apply(frame) #Benutzung des Subtrahierer
    try:
        ret,imBin= cv2.threshold(fgmask,200,255,cv2.THRESH_BINARY)
        #Öffnen (erode->dilate) Ton entferen
        mask0 =  cv2.morphologyEx(imBin ,  cv2.MORPH_OPEN, kernelOp2)
        #mask1 =  cv2.morphologyEx(imBin , cv2.MORPH_OPEN, kernelOp1)
        #mask2 =  cv2.morphologyEx(imBin,  cv2.MORPH_OPEN, kernelOp1)
        #Closing (dilate -> erode) para juntar regiones blancas.
        #mask3 =  cv2.morphologyEx(mask , cv2.MORPH_CLOSE,  kernelCl)
        #mask4 =  cv2.morphologyEx(mask1 , cv2.MORPH_CLOSE, kernelCl1)
        mask =  cv2.morphologyEx(mask0 , cv2.MORPH_CLOSE, kernelCl2)
    except:
        #Wenn keine weiteren Frames mehr folgen
        print('EOF')
        break

    maskOriginal=mask

    contours0, hierarchy = cv2.findContours(mask,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)
    # Falls die Kontur zu groß ist soll Sie halbiert werden
    mask2_flag=0
    for cnt in contours0:
        area = cv2.contourArea(cnt)
        if area > areaTH:
            M = cv2.moments(cnt)
            cx = int(M['m10']/M['m00'])
            cy = int(M['m01']/M['m00'])
            x,y,w,h = cv2.boundingRect(cnt)
            if w > wmax:
                mask2 = cv2.line(mask, ((x+w/2), 0), ((x+w/2),640),(0,0,0), 10)
                mask2_flag=1

    if mask2_flag==0:
        mask2=mask

    cv2.imshow('Mask su linija',mask2)
    cv2.imshow('mask to open',mask0)
    cv2.imshow('Mask pradinis tik veliau',maskOriginal)
    cv2.imshow('pradinis substraction',imBin)



    contours0, hierarchy = cv2.findContours(mask2,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)
    for cnt in contours0:
        cv2.drawContours(frame, cnt, -1, (0,255,0), 3, 8)
        area = cv2.contourArea(cnt)

            #################
            #   Prüfen ob die Person sich noch im Rahmen befindet, ansonsten Rahmen löschen
            #################
        for i in persons:
            i.updateDingimas(i.getDingimas()+1) #Zählt wie viele Frames das Objekt hat (nicht aktualisiert)
            if i.getDingimas() > 25:
                persons.remove(i)

        if area > areaTH:
            #################
            #   Objekto sekimas    
            #################            
            M = cv2.moments(cnt)
            cx = int(M['m10']/M['m00'])
            cy = int(M['m01']/M['m00'])
            x,y,w,h = cv2.boundingRect(cnt)

            print('x{} y{} w{} h{}'.format( x, y, w, h))


            new = True
            for i in persons:
                if abs(x-i.getX()) <= w_margin and abs(y-i.getY()) <= h_margin:
                    new = False
                    i.updateCoords(cx,cy)
                    i.updateDingimas(0) # nuresetina dingima
                    break
            if new == True:
                p = Person.MyPerson(pid,cx,cy, max_p_age)
                persons.append(p)
                pid += 1

            cv2.circle(frame,(cx,cy), 5, (0,0,255), -1)
            img = cv2.rectangle(frame,(x,y),(x+w,y+h),(0,255,0),2)
            cv2.drawContours(frame, cnt, -1, (0,255,0), 3)
            cv2.imshow('img',img)

    #########################
    # Trajektorien-Abbildung
    #########################
    for i in persons:
        if len(i.getTracks()) >= 2:
            pts = np.array(i.getTracks(), np.int32)
            pts = pts.reshape((-1,1,2))
            frame = cv2.polylines(frame,[pts],False,i.getRGB())
#        if i.getId() == 9:
#            print str(i.getX()), ',', str(i.getY())
         #################
         #   Prüfen ob die Linie überquert wurde  #
         #################
        if i.getDir() == None:
            i.kurEina( pts_L2[0,1] ,pts_L1[0,1])   #      def kurEina(bSottom_line,top_line):
            if i.getDir() == 'up':
                cnt_up+=1
                print('Timestamp: {:%H:%M:%S} UP {}'.format(datetime.datetime.now(), cnt_up))
            elif i.getDir() == 'down':
                cnt_down+=1
                print('Timestamp: {:%H:%M:%S} DOWN {}'.format(datetime.datetime.now(), cnt_down))



        cv2.putText(frame, str(i.getId()),(i.getX(),i.getY()),font,0.7,i.getRGB(),1,cv2.LINE_AA)

    #########################
    # Darstellung
    #########################
    str_up='UP: '+ str(cnt_up)
    str_down='DOWN: '+ str(cnt_down)
    frame = cv2.polylines( frame, [pts_L1], False, line_down_color,thickness=4)
    frame = cv2.polylines( frame, [pts_L2], False, line_up_color,thickness=4)
    cv2.putText(frame, str_up, (10,50), font,1,(0,0,255), 2,cv2.LINE_AA)
    cv2.putText(frame, str_down, (10,100), font,1,(255,0,0), 2,cv2.LINE_AA)


    cv2.imshow('Frame',frame)


    #cv2.imwrite("img/frame %d.jpg" % counter, frame)
    #counter=counter+1

    #Verlassen mit Q odder ESC
    k = cv2.waitKey(30) & 0xff
    if k == 27:
        break

cap.release() #release video file
cv2.destroyAllWindows() #close all openCV windows
Um das Beispiel zu visualisieren hier meine "Paint" Skizze des späteren Szenario:
Bild

Hoffe sehr das mir hier jemand weiterhelfen kann.

Liebe Grüße
Timo
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Was soll das My bei MyPerson? Ich dachte, es sollen beliebige Personen erkannt werden und nicht nur Du selbst.
Das Klassenattribut tracks ist unsinnig, da es ja auch gleich von einem Instanzattribut gleichen Namens überschrieben wird.
Die ganzen Getter und Setter sind überflüssig, da Du ja direkt die Attribute abfragen kannst.
Methodennamen werden, wie Variablen und Attribute auch, komplett klein geschrieben.

`state` kann die Werte "0" und "1" als String annehmen? Warum Strings? Warum 0 und 1 und nicht False und True? Und Warum überhaupt einen State? Was sagt der aus? Um es kurz zu machen: der State ist überflüssig, da `dir`, also direction das selbe und noch mehr angibt.

Das mit dem update_coords verstehe ich noch nicht. Warum gibt es ein x/y und das vorherige x/y-Pärchen wird dann in tracks gespeichert. Warum nicht gleich alle Koordinaten in Tracks schreiben?

cross_top und cross_bottom prüfen nicht, ob tracks genug Einträge hat.

Der Name kurEina ist total kryptisch. Die Funktion liefert einmal None implizit und einmal False explizit als Rückgabewert. Was soll das?

`age_one` scheint wohl ein `increase_age` zu sein.

`MultiPerson` macht irgendwie gar keinen Sinn. Ist das nur eine Liste von Personen?

Code: Alles auswählen

from random import randint

class Person:
    def __init__(self, id, x, y, max_age):
        self.id = id
        self.x = x
        self.y = y
        self.tracks = []
        self.rgb = (randint(0,255), randint(0,255), randint(0,255))
        self.done = False
        self.age = 0
        self.max_age = max_age
        self.direction = None
        self.linija1 = None
        self.linija2 = None
        self.dingimas=0

    def update_coords(self, x, y):
        self.age = 0
        self.tracks.append([self.x,self.y])
        self.x = x
        self.y = y

    def set_done(self):
        self.done = True

    def timed_out(self):
        return self.done

    def going_up(self, mid_start, mid_end):
        if len(self.tracks) >= 2:
            if self.direction is None:
                if self.tracks[1][1] > mid_end and self.tracks[-2][1] <= mid_start: #Überqueren der Linie
                    self.direction = 'up'
                    return True
        return False

    def going_down(self,mid_start,mid_end):
        if len(self.tracks) >= 2:
            if self.direction is None:
                if self.tracks[1][1] < mid_start and self.tracks[-2][1] >= mid_end: #Überqueren der Linie
                    self.direction = 'down'
                    return True
        return False

    def cross_bottom(self, bottom_line):
        if self.tracks[0][1] < bottom_line < self.tracks[-1][1]: #Die Linie von unten überqueren
            if self.linija1 != 'top':
                self.linija1 = 'bottom'
            else:
                self.linija2 = 'bottom'
        if self.tracks[0][1] > bottom_line > self.tracks[-1][1]: #gekreuzte Linie ist vorbei
            if self.linija1!='top':
                self.linija1 = 'bottom'
            else:
                self.linija2 = 'bottom'
                

    def cross_top(self, top_line):
        if self.tracks[0][1] < top_line < self.tracks[-1][1]: #Die Linie von oben überqueren
            if self.linija1!='bottom':
                self.linija1 = 'top'
            else:
                self.linija2 = 'top'
        if self.tracks[0][1] > top_line > self.tracks[-1][1]: #Die Linie von oben überqueren
            if self.linija1!='bottom':
                self.linija1 = 'top'
            else: 
                elf.linija2 = 'top'        
            
    def kur_eina(self,bottom_line,top_line):
        if len(self.tracks) >= 2:
            if self.dir == None:
                self.cross_bottom(bottom_line)
                self.cross_top(top_line)
                if  self.linija1== 'top' and self.linija2== 'bottom':
                    self.direction = 'down'
                elif  self.linija1== 'bottom' and self.linija2== 'top':
                    self.direction = 'up'
        else:
            return False
        
    def increase_age(self):
        self.age += 1
        if self.age > self.max_age:
            self.done = True
Der zweite Code ist mir zu Spaghetti. 180 Zeilen ohne irgendeine Funktion ist einfach zu viel.
Eine Person `i` zu nennen, trägt nicht zur Lesbarkeit bei. Auch cnt als Abkürzung für contour ist mehr als verwirrend.
Benutzeravatar
Trittbrettfahrer
User
Beiträge: 2
Registriert: Donnerstag 18. März 2021, 10:31
Wohnort: Köln

Vielen Dank für das Feedback. Ich habe mir schon gedacht das ein zusammen gewürfelter Code nicht das gelbe vom Ei ist. Ich denke ich fange einfach nochmal von vorne an und lerne dabei vielleicht auch nochmal etwas mehr.
Wichtig für mich ist noch eine Einschätzung: Ist ein solches Projekt auf dem Raspberry Pi realisierbar (Leistung, Fps)?
Falls jemand eine Literatur Empfehlung für OpenCV hat, würde ich mich sehr freuen. Python Bücher im generellen habe ich mehr als genug. Habe bisher nur Englische Literatur gefunden, die teils sehr wirr war. Zudem habe ich noch einen netten Inder auf YouTube, der sehr praxisnah die Funktionen erklärt. Ein gut geschriebenes Buch ist mir jedoch lieber, daher meine Frage. Vielleicht kann mir auch noch jemand verraten, ob es für mein konkretes Projekt schlauer ist in Python oder C++ das Programm zu realisieren.
LG
Timo
einfachTobi
User
Beiträge: 491
Registriert: Mittwoch 13. November 2019, 08:38

PyImageSearch ist generell eine gute Anlaufstelle für openCV-Projekte und hier wird sogar genau das beschrieben, was du vor hast. Da kannst du dir sicherlich gut was abschauen, wenn nicht sogar vollständig übernehmen. Viele Posts und weitere Beschreibungen sind auch in seinen Büchern zu finden. Die Umsetzung ist dort meist ebenfalls auf einem Pi. Probiers einfach mal aus. Stärkere Hardware lässt sich dann immer noch anschaffen.
Antworten