Détection de personnes dans une vidéo

Utilisons l'algorithme HOG de OpenCV pour détecter des personnes en temps réel dans un flux vidéo.

real time person detection with colin
Vacances à Bandol

Introduction

Nous allons écrire un petit programme capable de détecter des personnes dans un flux vidéo, en temps réel (ou presque, ça dépendra de la vitesse de votre CPU).

Dans mon article sur la détection d'objets avec darknet , nous avons vu comment détecter des objets appartenant à 80 catégories (personne, voiture, camion, plante en pot, girafe...) grâce au réseau de neurones profond YOLOv3 .

YOLOv3 est l'un des algoritmes de détection les plus aboutis. Il est extrêmement précis, et il est rapide pourvu qu'il soit évalué sur de puissants GPUs.

Cependant, même avec une GeForce GTX 1080 Ti, la détection d'objets dans une seule image prend 200 ms. Or, pour une détection en temps réel, il nous faudrait descendre à 40 ms / image ou moins, pour pouvoir traiter un flux vidéo à 24 images / s.

De plus, les stations de deep learning puissantes sont chères et consomment une grande quantité d'énergie. Elles ne sont donc pas du tout adaptées si votre but est de construire un petit système de vidéosurveillance pour votre maison qui doit rester en ligne continuellement.

Donc ici, nous allons pour une fois tourner le dos au deep learning et utiliser un algorithme simple et rapide, même sur un CPU.

Dans cet article, vous apprendrez :

  • Comment installer OpenCV, qui fournit des outils simples gérer les entrées et sorties vidéo, et pour les analyser.
  • Comment écrire un petit script pour faire de la détection de personnes en temps réel avec votre webcam ou dans une de vos vidéos, avec l'algorithme HOG (Histograms of Oriented Gradients);
  • Comment HOG fonctionne.

Installation d'OpenCV

OpenCV est la librairie open source de référence pour la vision par ordinateur, et elle est extrêmement puissante. Voici quelques exemples d'applications:

  • entrées / sorties vidéo
  • reconstruction de la 3D
  • analyse vidéo
  • détection d'objets
  • assemblage de panoramas à partir d'un jeu de photos
  • ...

Si vous souhaitez apprendre à utiliser cet outil, vous pourriez démarrer par le tutoriel d'OpenCV , ou jeter un oeil à l'excellent blog d'Adrian Rosebrock . C'est justement là que j'ai découvert cet outil.

Pour l'installation, nous utiliserons Anaconda. D'abord, installez Anaconda pour python 3.X

Ensuite, nous allons ajouter les packages suivant à anaconda: opencv numpy matplotlib.

Si vous savez comment utiliser la ligne de commande, vous pouvez le faire en tapant cette commande :

conda install opencv numpy matplotlib

Sinon, utilisez l'Anaconda Navigator.

Accéder à la webcam

Avec OpenCV, c'est tout simple d'accéder au flux vidéo en provenance de la webcam. Il vous suffit de récupérer ce petit script et de l’exécuter avec python :

import numpy as np
import cv2

cv2.startWindowThread()
cap = cv2.VideoCapture(0)

while(True):
    # récupération de la prochaine image
    ret, frame = cap.read()
    # affichage de l'image
    cv2.imshow('frame',frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        # on s'arrête si l'utilisateur tape q.
        # attention la fenêtre vidéo doit être active.
        break

cap.release()
cv2.destroyAllWindows()
# ce qui suit est nécessaire sous macos, 
# mais peut-être pas sur d'autres plateformes: 
cv2.waitKey(1)

Vous devriez voir une fenêtre apparaître avec l'image de votre webcam :

Essayons maintenant de manipuler le flux vidéo. La vidéo est lue image par image, et nous allons éditer chaque image avant de l'afficher. Ajoutez les lignes suivantes avant l'affichage de l'image :

    # on passe en noir et blanc:
    frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
    # application d'un seuil. Tous les pixels avec un niveau 
    # supérieur à 80 sont montrés en blanc, et les autres en noir:
    ret,frame = cv2.threshold(frame,80,255,cv2.THRESH_BINARY)

People detection

OpenCV fournit une implémentation pour une méthode rapide de détection de personne appelée HOG (Histograms of Oriented Gradients).

Cette méthode est entraînée à la détection de piétons, c'est à dire des humains se tenant debout, et complètement visibles. Il ne faut donc pas s'attendre à ce que cet algorithme fonctionne dans d'autres cas.

Avant de discuter la méthode, essayons-là. Modifiez votre script de la façon suivante :

import numpy as np
import cv2
 
# initialisation du HOG:
hog = cv2.HOGDescriptor()
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())

cv2.startWindowThread()

# ouverture du flux vidéo de la webcam
cap = cv2.VideoCapture(0)

# la sortie sera écrite dans le fichier output.avi
out = cv2.VideoWriter(
    'output.avi',
    cv2.VideoWriter_fourcc(*'MJPG'),
    15.,
    (640,480))

while(True):
    # capture image par image
    ret, frame = cap.read()

    # réduction de l'image pour une détection plus rapide
    frame = cv2.resize(frame, (640, 480))
    # passage en noir et blanc, également pour accélerer 
    # la détection
    gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
    
    # détection des personnes dans l'image. 
    # retourne les coordonnées de la boîte encadrant 
    # les personnes détectées
    boxes, weights = hog.detectMultiScale(frame, winStride=(8,8) )

    boxes = np.array([[x, y, x + w, y + h] for (x, y, w, h) in boxes])

    for (xA, yA, xB, yB) in boxes:
        # affichages des boîtes sur l'image couleur
        cv2.rectangle(frame, (xA, yA), (xB, yB),
                          (0, 255, 0), 2)
    
    # écriture de la vidéo avec les boîtes
    out.write(frame.astype('uint8'))
    # affichage de l'image résultante
    cv2.imshow('frame',frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# quand on a terminé: 
# on termine la capture
cap.release()
# on termine l'écriture 
out.release()
# et on ferme la fenêtre
cv2.destroyAllWindows()
cv2.waitKey(1)

Vous pouvez maintenant faire tourner ce script.

Avec ces paramètres, la détection se fait quasiment en temps réel sur ma machine. Vous verrez dans la vidéo ci-dessous que le détecteur fonctionne mieux si la personne n'est pas trop près de la caméra. Si la personne est trop près, plusieurs boîtes superposées sont généralement affichées.

https://www.youtube.com/watch?v=4iXMoJEVK4g&feature=youtu.be

Cependant, ces performances sont plus que suffisantes pour un petit système de vidéosurveillance, qui sera le sujet d'un futur article.

HOG : Explication

Vous êtes déjà capable de détecter des gens dans un flux vidéo, et vous pouvez tout à fait vous arrêter là si vous le souhaitez.

Mais si vous voulez savoir comment l'algorithme HOG fonctionne, continuez votre lecture, j'ai essayé d'expliquer ça en termes simples.

Les performances de l'algorithme sont montrées dans le papier original par Dalal et Triggs. Cependant, pour vraiment comprendre le fonctionnement de l'algorithme, j'ai dû creuser les références, et notamment cet article concernant la reconnaissance de gestes de la main . Vous pouvez aussi regarder ce très bon article de blog .

Pour comprendre comment HOG ( Histograms or Oriented Gradients ) fonctionne, il est d'abord nécessaire de comprendre ce qu'est un gradient, et ce qu'est un histogramme. Voyons ça.

Qu'est-ce qu'un gradient?

Pour une fonction à une dimension dépendant de la variable x, le gradient est simplement la dérivée de la fonction. À un point donné, la dérivée donne la pente locale de la fonction.

Par exemple, considérons la fonction f(x)=x. La dérivée de cette fonction est f'(x) = 1. Cette fonction est une ligne droite, et sa dérivée est constante, ce qui veut dire que la pente est constante.

f(x) = x et sa dérivée, f'(x) = 1

Si nous prenons maintenant f(x) = x^2, la dérivée est f(x) = 2x. Pour x<0, la dérivée est négative, et la fonction décroît. Pour x>0, la dérivée est positive et la fonction croît. Pour x=0, la dérivée est 0. Cela veut dire que la pente locale en 0 est nulle. En d'autres termes, la fonction est localement plate en ce point:

f(x) = x^2 et sa dérivée, f'(x) = 2x

En 2D, dans le plan (x,y), une fonction de x et y est une surface donnant l'altitude en chaque point. Le gradient est une généralisation de la dérivée : En un point (x,y) donné, le gradient est orienté en direction de la ligne de plus grande pente, et sa norme est la pente du plan tangent à la surface en ce point. Voici une illustration de l'article wikipedia :

Gradient de deux fonction 2D. Dans ce cas particulier, le noir correspond aux valeurs élevées de la fonction. Au centre de l'image de gauche, le gradient n'est pas affiché, mais il vaudrait 0.

Revenons à nos moutons. Dans une image noire et blanche, le niveau de gris est analogue à l'altitude, et le gradient est une mesure de la vitesse et de la direction du changement de niveau de gris. Une bord dans l'image (une transition de noir à blanc) correspond à un gradient élevé, perpendiculaire au bord. Dans une image couleur, on peut calculer un gradient pour chaque niveau de couleur (RGB), et par exemple choisir le gradient maximum observé sur les trois niveaux de couleur.

Mais pourquoi dit-on que les gradients sont orientés? En effet un gradient est toujours orienté... Cela veut simplement dire que la méthode s'intéresse à la direction du gradient. Et ça donne surtout un joli acronyme : HOG

Qu'est-ce qu'un histogramme?

C'est une structure utilisée pour compresser les données et représenter leur distribution de probabilité. Un histogramme peut avoir de nombreuses dimensions mais en pratique, les histogrammes à 1D ou 2D sont généralement utilisées. Dans HOG, les gradients sont stockés dans un histogramme 1D, et nous allons donc nous focaliser sur ce cas.

Prenez un tableau de valeurs : [1, 1.5, 2.2, 3.5, 3.5, 3.6, 4.1].

On définit l'histogramme en décrivant ses bins. Les bins sont des sortes de paniers qui comptent le nombre d'entrées avec une valeur tombant dans l'intervalle du bin.

Vous pouvez taper cela dans ipython pour créer et afficher un histogramme :

import numpy as np
import matplotlib.pyplot as plt

values = [1.1, 1.5, 2.2, 3.5, 3.5, 3.6, 4.1]
plt.hist(values, bins=4, range=(1,5))
plt.show()

Vous obtenez la chose suivante :

Un histogramme avec 4 bins. Le premier bin a deux entrées (1.1, 1.5). Le deuxième bin une entrée (2.2), le troisième bin trois entrées (3.5, 3.5, 3.6), et le dernier une entrée (4.1).

Il est également possible de pondérer chaque valeur. Quand la valeur entre dans l'histogramme, sa contribution au bin est donnée par le poids appliqué, qui est 1 par défaut. Voici un histogramme pondéré :

import numpy as np
import matplotlib.pyplot as plt

values = [1.1, 1.5, 2.2, 3.5, 3.5, 3.6, 4.1]
weights = [1., 1., 3., 1.2, 1.4, 1.1, 0.2]
plt.hist(values, bins=4, range=(1,5), weights=weights)
plt.show()
Un histogramme pondéré.

Histograms of Oriented Gradients

L'idée de base de la méthode est la suivante :

  • L'image est scannée par une fenêtre de détection de taille variable.
  • Pour chaque position et taille de la fenêtre, celle-ci est divisée en cellules . Les cellules sont en pratique assez petite : elle ne contiennent qu'une petite portion de la personne à détecter, peut-être le bord d'un bras ou le haut de la tête.
  • Dans chaque cellule, un gradient est calculé pour chaque pixel, et les gradients sont utilisés pour remplir un histogramme : la valeur est l'angle du gradient, et le poids la norme de ce vecteur.
  • Les histogrammes de toutes les cellules sont regroupés et donnés à un algorithme de machine learning qui décide si cette fenêtre contient une personne ou pas.

Nous parlerons du dernier point dans la section suivante. Ici, nous allons d'abord voir comment l'histogramme de gradients est construit pour une cellule donnée, avec un petit exemple :

import numpy as np
import matplotlib.pyplot as plt
import cv2

# une image: 
cell = np.array([
    [0, 1, 2, 5, 5, 5, 5, 5],
    [0, 0, 1, 4, 4, 5, 5, 5],
    [0, 0, 1, 3, 4, 5, 5, 5],
    [0, 0, 0, 1, 2, 3, 5, 5],
    [0, 0, 0, 0, 1, 2, 5, 5],
    [0, 0, 0, 0, 0, 1, 3, 5],
    [0, 0, 0, 0, 0, 0, 2, 5],
    [0, 0, 0, 0, 0, 0, 1, 3],
    ],dtype='float64')

# calcul des gradients dans les directions x et y
gradx = cv2.Sobel(cell, cv2.CV_64F,1,0,ksize=1)
grady = cv2.Sobel(cell, cv2.CV_64F,0,1,ksize=1)
# passage en coordonnées circulaires: 
norm, angle = cv2.cartToPolar(gradx,grady,angleInDegrees=True)

plt.figure(figsize=(10,5))

# affichage de l'image
plt.subplot(1,2,1)
plt.imshow(cell, cmap='binary', origin='lower')

# affichage de la norme des gradients:
plt.subplot(1,2,2)
plt.imshow(norm, cmap='binary', origin='lower')
# et indication par une flèche de la direction des gradients: 
q = plt.quiver(gradx, grady, color='blue')
plt.savefig('gradient.png')
plt.show()

Vous obtenez les figures suivantes. On voit que dans la figure de droite, qui représente les gradients, les surfaces plates disparaissent, et que la bordure est clairement visible.

Gauche: Image de la cellule. Droite: les gradients correspondants. Les gradients sont représentés par une flêche, et leur norme est également montrée par les niveaux de gris.

Traçons maintenant l'histogramme de gradients pour cette cellule :

plt.hist(angle.reshape(-1), weights=norm.reshape(-1), bins=20, range=(0,360))
plt.show()
Histogramme des angles des gradients. Chaque entrée est pondérée par la norme du gradient.

Dans cette histogramme, les entrées à 0 correspondent aux gradients dirigés vers la droite. Il y a une seule flêche dirigée vers le bas, visible dans le bin [270;290[.

Si plusieurs pixels de la cellule ont des gradients d'orientation similaire, ils contribuent aux mêmes bins. De plus, les gradients importants contribuent plus. Ainsi, le pic à droite correspond à la bordure visible dans la cellule.

Histogrammer les gradients de cette façon permet donc de reconnaître les bordures facilement dans la cellule.

Cela dit, cet histogramme est pour une unique cellule. Il nous faut donc une procédure permettant d'analyser toutes les cellules de la fenêtre de détection courante pour savoir si celle-ci contient ou pas une personne.

Classification de la fenêtre de détection

Nous avons donc un HOG pour chacune des cellules de la fenêtre de détection. Tous ces HOGs sont concaténés en un grand tableau de valeurs. Par exemple, si la fenêtre de détection a 8x16 = 128 cellules et que chaque HOG a 20 bins, on se retrouve avec 2560 valeurs.

N'importe quel type d'algorithme de machine learning peut en principe être utilisé pour prendre une décision (y'a t'il une personne ou pas) à partir de ces valeurs

Cependant, comme ce problème comporte un grand nombre de dimensions (2560 dans notre exemple), et qu'il n'y avait pas beaucoup d'images disponibles pour l'entraînement du HOG, Dalal et Triggs décidèrent d'utiliser un Support Vector Machine (SVM) linéaire. C'est aussi ce qui est fait dans l'implémentation d'OpenCV.

Pour expliquer les SVMs, il est nécessaire de manipuler des outils mathématiques avancés, et ce n'est pas du tout le but de ce blog. Mais si vous êtes intéressé(e) et que vous êtes au moins au niveau master en maths ou en physique, vous pouvez suivre l'excellent cours d'Andrew Ng à ce sujet.

Et maintenant?

Vous savez maintenant :

  • installer OpenCV
  • écrire un petit script permettant de détecter des personnes dans une vidéo, avec l'algorithme HOG

Et vous connaissez également le principe de base de l'algorithme HOG.


N'hésitez pas à me donner votre avis dans les commentaires ! Je répondrai à toutes les questions.

Et si vous avez aimé cet article, vous pouvez souscrire à ma newsletter pour être prévenu lorsque j'en sortirai un nouveau. Pas plus d'un mail par semaine, promis!

Retour


Encore plus de data science et de machine learning !

Rejoignez ma mailing list pour plus de posts et du contenu exclusif:

Je ne partagerai jamais vos infos.
Partagez si vous aimez cet article: