Nous avons vu la semaine dernière comment suivre un objet unique dans une image. Pour ce faire, nous avions réalisé une détection d’objet à chaque trame et avons enregistré les positions successives afin de générer une queue. Le problème que nous nous sommes posé à la fin de l’article était de savoir si nous étions sûrs que ce soit le même objet et comment réaliser un suivi multi-objet. C’est ce que nous allons voir dans l’article d’aujourd’hui.

Suivi multi-objet

Suivi multi-objets, vidéo initiale

Suivi multi-objets (Vidéo source)

Reprenons la vidéo de la semaine dernière et ajoutons un nouvel objet :
Maintenant, posons-nous la question : comment associer chaque détection à la détection précédente ? La couleur ou la forme pourrait être des solutions. Cependant utiliser l’une ou l’autre des solutions est uniquement valable dans le cas où les objets à suivre sont différents.
Nous allons plutôt nous intéresser à la position de chaque objet. En effet, si un objet est détecté à l’instant t, il y a de forte chance qu’il se trouve dans le voisinage à l’instant t+1. Nous allons donc calculer la distance entre le centre de l’objet à l’instant t et le centre des détections à l’instant t+1. Plus la distance est faible, plus la probabilité que ce soit le même objet est grande.
Nous commencerons donc par créer une classe « position_tracker » qui nous permettra d’associer les détections successives, puis nous changerons quelque peu le programme de la semaine dernière afin de prendre en compte le suivi de plusieurs objets.
Afin de favoriser la compréhension, notre méthode se basera sur la position de chaque objet et le calcul de la distance euclidienne entre un objet et les détections à l’instant suivant.

Algorithme de suivi des centres

Nous développons donc un algorithme prenant comme entrée les coordonnées des centres des détections à l’instant t. La première étape sera de calculer la distance entre chaque objet connu (instant t-1) avec les nouvelles détections (instant t). La seconde étape est de mettre à jour les positions de chaque objet en associant la position de chaque objet avec celle de la détection la plus proche.
Cependant, comme chaque objet n’est pas forcément présent à l’instant t, nous devons prendre en compte certains cas : l’apparition d’un nouvel objet et la disparition d’un objet
À cause de la structure du code est des données, nous commencerons par ces derniers cas, avant de traiter le problème d’association des objets.
Commençons par initialiser notre classe. Nous créons un fichier position_tracker.py et nous commençons par importer les bibliothèques nécessaires à l’algorithme.

from collections import OrderedDict
import math

class position_tracker():
    def __init__(self):
        self.prochainID = 0
        self.objets=OrderedDict()
        self.max_perdu=30

 

Nous importons la classe OrderedDict de la bibliothèque collections (l. 1), qui nous permettra de structurer les données de nos objets. La bibliothèque math est également importée (l. 2) afin de calculer la distance entre chaque objet.

Déclarons maintenant notre classe (l. 4) et initialisons-la aux lignes 5 à 8. Nous aurons besoin de connaître l’identifiant du prochain objet (l. 6) qui nous permettra d’associer un identifiant à un objet nouvellement détecté. Nous déclarons ensuite le type de donnée (OrderedDict) pour stocker les informations sur nos objets (l. 7). Ce dictionnaire utilisera l’identifiant de l’objet comme clef et les coordonnées (x,y) de l’objet, ainsi que le nombre de trames que l’on a perdu l’objet (si c’est le cas), comme valeur. Nous pouvons voir sa structure dans le tableau suivant.

ID objetPositionNombre de trame disparu

Enfin, nous initialisons la variable max_perdu à 30 (l. 8) qui nous permettra de déclarer un objet comme perdu s’il n’est pas détecté sur 30 images.

Apparition et disparition d’un objet

Maintenant que l’initialisation est prise en compte, passons à nos deux cas particuliers.

from collections import OrderedDict
import math

class position_tracker():
    def __init__(self):
        self.prochainID = 0
        self.objets=OrderedDict()
        self.max_perdu=30

    def nouvel_objet(self,position):
        self.objets[self.prochainID]=position,0
        self.prochainID += 1

    def supp_objet(self,ID):
        self.objets.pop(ID)

Le premier est l’apparition d’un nouvel objet. Nous définissons donc la fonction nouvel_objet (l. 10) qui prend en entrée la position du nouvel objet. Cette position est ajoutée au dictionnaire objets avec l’identifiant prochainID et le nombre de trames où l’objet a disparu est initialisé à 0. On vient de le découvrir, il n’est donc pas encore perdu. Enfin, on incrémente de 1 la variable prochainID qui sera utilisée pour le prochain nouvel objet.

Le second cas, la suppression d’un objet (l. 14) sera utilisé dans le cas où un objet a disparu pendant plus de 30 images. Nous utilisons la méthode pop de la classe OrderedDict() qui permet de supprimer du dictionnaire la ligne correspondant à notre objet disparu.

Calcul de la distance

Nous pouvons maintenant passer à la définition de la fonction nous permettant de calculer la distance entre un objet et une détection.

    def distance(self,position,ID):
        dist=math.sqrt((position[0]-self.objets[ID][0][0])**2+(position[1]-self.objets[ID][0][1])**2)
        return dist

 

La fonction distance prend entrée la position d’une détection ainsi que l’identifiant de l’objet dont nous souhaitons connaître la distance. Cette distance est la distance euclidienne entre les deux points et est définie par la formule suivante.

Suivi de plusieurs objet: distance euclidienne

Mise à jour des positions

Maintenant que nos positions sont prêtes, nous pouvons passer au cœur même de l’algorithme.

Nous commençons, encore une fois, par les cas particuliers: le cas où aucun objet n’existe (C’est le cas lors de la première trame.) et le cas où aucun objet n’est détecté dans l’image.

    def update(self, centre):

        if len(centre)==0:
            for ID in self.objets.keys():
                self.objets[ID]=self.objets[ID][0],self.objets[ID][1]+1
                if self.objets[ID][1] > self.max_perdu:
                    self.supp_objet(ID)
            return self.objets  

        if len(self.objets) ==0:
            for i in range(len(centre)):
                self.nouvel_objet(centre[i])

Si aucun objet n’est détecté dans l’image (l. 23), pour chaque objet détecté précédemment (l. 24), on incrémente le nombre de trames dont chaque objet est perdu (l. 25) . Du fait que les valeurs du dictionnaire soient des tuples, il est nécessaire de spécifier la position et le nombre de trames. Un objet pouvant disparaitre au bord de l’image ou derrière un autre objet, on conserve la dernière position connue, dans le cas où l’objet réapparaîtrait dans le voisinage. Dans le cas où l’objet a disparu depuis 30 images (l. 26), on le supprime (l. 27). Enfin, si aucun n’objet n’est détecté dans l’image, rien ne sert de mettre à jour les objets: On retourne les objets (l. 28).

Dans le cas où aucun objet n’est connu, mais que l’on a des détections (l. 30), on crée un nouvel objet pour chaque centre détecté.

Cas général

Passons maintenant au cas général.

from collections import OrderedDict
import math

class position_tracker():
    def __init__(self):
        self.prochainID = 0
        self.objets=OrderedDict()
        self.max_perdu=30

    def nouvel_objet(self,position):
        self.objets[self.prochainID]=position,0
        self.prochainID += 1

    def supp_objet(self,ID):
        self.objets.pop(ID)

    def distance(self,position,ID):
        dist=math.sqrt((position[0]-self.objets[ID][0][0])**2+(position[1]-self.objets[ID][0][1])**2)
        return dist

    def update(self, centre):

        if len(centre)==0:
            for ID in self.objets.keys():
                self.objets[ID]=self.objets[ID][0],self.objets[ID][1]+1
                if self.objets[ID][1] > self.max_perdu:
                    self.supp_objet(ID)
            return self.objets  

        if len(self.objets) ==0:
            for i in range(len(centre)):
                self.nouvel_objet(centre[i])
        else:
            used_ID=[]
            for position in centre:
                dist=[]
                min_dist=10000
                for ID in self.objets.keys():
                    dist.append((self.distance(position,ID),ID))
                for distance in dist:
                    if distance[0]<=min_dist:
                        min_dist=distance[0]
                        selected_ID=distance[1] 
                self.objets[selected_ID]=position,self.objets[ID][1]
                used_ID.append(selected_ID)
            
            #Si un objet à disparu
            x=[x for x in self.objets.keys() if x not in used_ID]
            for perdu in x:
                self.objets[perdu]=self.objets[perdu][0],self.objets[perdu][1]+1
                if self.objets[perdu][1] > self.max_perdu:
                    self.supp_objet(perdu)

            #Si un objet apparait
            if len(centre)>len(self.objets.keys()):
                pos=[x for x in centre if x not in self.objets.values()]
                self.nouvel_objet(pos[0])

        return self.objets

Nous commençons par définir une liste: used_ID (l. 34) qui nous permettra de stocker les ID qui ont déjà été associés et ainsi augmenter le nombre de frames où un objet a disparu le cas échéant.

Pour chaque centre détecté (l.35), nous définissons une liste dist qui nous permettra de stocker toutes les distances entre une détection et chaque objet (l. 38 – 39). Nous initialisons également une variable min_dist (l.36), qui nous permettra de connaître l’objet (l. 43) ayant la distance minimale (l. 40-42) avec une détection.

Lorsque nous avons testé chaque objet pour une détection, nous mettons à jour sa position (l. 44) et nous ajoutons l’ID de l’objet associé à la liste used_ID (l. 45).

Maintenant que chaque détection est associée à un objet connu, nous pouvons passer aux cas particuliers à savoir: un objet disparaît ou apparaît.

Prenons le premier cas (l. 47-52). Pour chaque objet connu qui n’a pas été utilisé (l. 48-49), nous conservons sa position, mais augmentons le nombre de frames dont nous avons perdu l’objet (l. 50). Si ce nombre de frames dépasse le nombre de frames maximum pour considérer un objet disparu (l. 51) alors nous le supprimons des objets détectés (l. 52).

Dans le second cas (l. 54-57), le nombre de détections est supérieur au nombre d’objets détectés (l.55) alors, pour chaque position non utilisée (l. 56), nous créons un nouvel objet (l.57).

Enfin, vu que nous avons traité tous les cas, nous pouvons renvoyer le dictionnaire des objets mis à jour. et nous en avons fini avec notre classe. Nous pouvons donc passer à l’implémentation.

Implémentation du suivi multi-objet.

Nous repartons du programme que nous avons fait la semaine dernière et nous le modifions légèrement. Je ne reprendrais pas l’explication de chaque ligne, vu que nous l’avons fait précédemment et nous nous concentrerons sur les modifications du programme. Si vous n’avez toujours pas lu l’article de la semaine dernière, vous pouvez vous rendre ici.

import cv2
import numpy as np
from position_tracker import position_tracker

cap=cv2.VideoCapture('tracking_2.mp4')
pt=position_tracker()
min_limite=(0,125,125)
max_limite=(255,255,255)

Nous commençons donc par appeler la classe position_tracker que nous venons de créer et qui se trouve dans le fichier position_tracker.py (l.3). Nous initialisons également notre tracker (l. 6) que nous appellerons par la suite  pt.

while cap.get(cv2.CAP_PROP_POS_FRAMES)!=cap.get(cv2.CAP_PROP_FRAME_COUNT)-1:
    ret,frame=cap.read()
    img_hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)
    masque=cv2.inRange(img_hsv,min_limite, max_limite)
    im2, contours, hierarchy=cv2.findContours(masque,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
    list_centre = []
    if len(contours)>0:
        for cnt in contours:
            x,y,w,h=cv2.boundingRect(cnt[0])
            cv2.rectangle(frame, (x, y),(x+w,y+h),(0, 255, 0), 3)
            centre=((int(x+w/2),int(y+h/2)))
            list_centre.append(centre)

La partie détection des objets ne change pratiquement pas, à part la création d’une liste list_centre qui remplace la liste point. Cette liste est vidée à chaque itération (l. 15) et sert à stocker toutes les détections dans une frame (l. 21) .

    objet=pt.update(list_centre)
    for i in objet.items():
        text = "ID {}".format(i[0])
        cv2.putText(frame, text, (i[1][0][0], i[1][0][1]),cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
    cv2.imshow('frame',frame)
    cv2.waitKey(1)

cap.release()
cv2.destroyAllWindows()

Maintenant que les nouveaux objets sont extraits, nous mettons à jour notre tracker et nous récupérons les objets (Identifiants et positions) (l. 23). Enfin pour chaque objet suivi, nous affichons l’identifiant correspondant (l. 25 – 26).

Résultat

Maintenant que notre tracker, ainsi que son implémentation sont finis, nous pouvons visualiser notre tracking.

Suivi de plusieurs objets

Résultat du suivi multi-objets

Nous avons dans cet article, implémenté une méthode de suivi multi-objets. Cette méthode ce base sur l’association des détections à l’instant t-1 avec celles de l’instant. Cependant, de nombreux cas n’ont pas été abordés : que se passe-t-il lorsque deux objets se frôlent (distances identiques), ou lorsqu’un objet passe derrière l’autre (occlusion). Nous essaierons d’y répondre la semaine prochaine.


0 commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.