La détection d’objets avec YOLO v4

Partager cet article
0

Dans la série sur les images nous avons vu comment étaient structurées et surtout comment manipuler les images. A la fin de la série nous avons même abordé les premiers concepts de réseaux de neurones convolutifs afin de classifier des images. L’idée était simple : trier des images par sujet. Simple à première vue … en tout cas quand l’image ne contient d’un seul sujet. Mais que se passe-t-il quand on a plusieurs sujets sur une photo ? quand on ne veut détecter qu’un seul type de sujet ou juste localiser un ou plusieurs objets ?

La question est tout de suite moins évidente n’est-ce pas ? je vous propose de survoler ce sujet qui m’a beaucoup interrogé quand je me suis lancé dans le Deep Learning, car il faut le reconnaitre il y a beaucoup d’articles sur le net sur la classification mais dés lors que l’on veut dépasser ce sujet, c’est tout de suite moins prolifique ou alors beaucoup plus technique.

Implémentation de YOLO

Nous allons voir dans cet article, comment avec le réseau de neurones YOLO nous pourrons très simplement détecter plusieurs objets dans une photo. L’objectif n’est pas d’entrer dans le détail de l’implémentation de ce réseau de neurones (beaucoup plus complexe qu’un simple CNN séquentiel) mais plutôt de montrer comment utiliser l’implémentation qui a été réalisée en C++ et qui se nomme Darknet.

Pour cela tout se trouve dans Github: https://github.com/AlexeyAB/darknet

Malheureusement, comme je l’ai dit plus haut l’implémentation de ce réseau a été réalisée en C++. Le binaire n’est donc pas portable d’une plateforme à l’autre, et donc il va nous falloir recompiler tout cela avant de pouvoir l’utiliser. Mais rassurez-vous c’est très simple et sera réalisé en une seule ligne de commande. Mais nous allons voir cela plus loin, pas de panique 😉

La classification

Nous l’avons vu et c’est en effet le premier pas que l’on fait quand on aborde les réseaux convolutifs (CNN). La classification consiste à déterminer ce que représente une image. Ce quelle représente s’appelle une classe. La liste des classes est finie (catégorielle) et la sortie du réseau de neurones aura autant de neurones que de classes. Chaque sortie présentant une probabilité de la classe pour l’image qui a été présentée en entrée.

Je ne vais pas m’attarder trop sur le sujet, pour plus de détails allez voir cet article.

Le soucis avec la classification est que le jeu de données ne doit comprendre que des données avec le sujet (ou sans bien sur) … sinon cela ne vous avancera pas à grand chose. Prenons un exemple concret avec la photo ci dessous.

Imaginez que vous crééz un réseau de neurones convolutif (CNN) qui permette de classifier les éléments que l’on trouve sur un bureau. Partons du principe que vous avez une liste de classes (6 au total) comme ceci : NOTEBOOK, LAPTOP, GLASS, DRIVE, PHONE, KEY, COFFEE, etc.

Votre réseau de neurones vous renverra en sortie quelque chose qui pourrait ressembler à cela :

  • NOTEBOOK: 97%
  • LAPTOP: 98%
  • GLASS: 1%
  • PHONE: 85%
  • DRIVE: 45%
  • KEY: 9%
  • COFFEE: 96%

Chaque pourcentage représentant les probabilité que l’objet (ou la classe) se trouve dans l’image. Voilà ce que l’on sait maintenant : nous avons dans cette image un ordinateur, un bloc note, un téléphone et un café. Mais à part cela (ce qui est déjà pas mal en fait), où se trouvent ces sujets ?

Principe de détection des objets

Nous y voila, nous sommes ravi de constater que des sujets ont été identifiés mais où sont-ils ? (d’ailleurs il peut y en avoir plusieurs), il nous faut maintenant les localiser.

Qui dit localisation, dit coordonnées !

Nous devons donc en plus de trouver les différentes probabilités de classes, renvoyer pour chacune un lot de coordonnées.

Pour détecter objets dans une même image l’approche originale fut de lancer des classifications en faisant glisser des plus petites fenêtres sur toute l’image … mais cette approche était longue et surtout imposait de relire plusieurs fois l’image source. L’idée fondamentale derrière YOLO est de ne faire qu’une seule passe (lecture) de l’image (YOLO = You Only Look Once). Résultat la détection est vraiment beaucoup plus rapide !

Préparation

Passons à la pratique … je vous propose d’utiliser dés maintenant ce réseau pour détecter des images. Pour cela nous allons utiliser

  • Google colab (comme ça nous aurons le même environnement d’exécution, et même mieux nus pourrons utiliser des GPUs gratuitement).
  • L’implémentation la plus répendue Darknet disponible sur Github : https://github.com/AlexeyAB/darknet

Vous n’aurez pas besoin de plus, un simple laptop basique vous suffira !

Création du notebook dans colab

Créez votre notebook dans Google colab, si vous avez oublié ou ne savez pas comment faire regardez l’article suivant.

Une fois le notebook créé, il faut activer les GPUs. Pour cela :

  1. Sélectionnez le menu: Exécution
  2. Choisissez Modifier le type d’exécution
  3. Sélectionnez GPU (Accélérateur matériel)

Préparation de Darknet

Dans Google colab, il faut maintenant importer le projet darknet, pour cela on peut utiliser les commandes directement dans les cellules en les préfixant par !

!git clone https://github.com/AlexeyAB/darknet
Cloning into 'darknet'...
remote: Enumerating objects: 14997, done.
remote: Total 14997 (delta 0), reused 0 (delta 0), pack-reused 14997
Receiving objects: 100% (14997/14997), 13.38 MiB | 11.68 MiB/s, done.
Resolving deltas: 100% (10194/10194), done.

Cela devraient prendre un peut de temps le temps d’importer dans votre environnement colab tout le contenu du projet.

Attention: vous devrez réimporter le projet à chaque ouverture du notebook.

Pour utiliser les GPUs qui sont à votre disposition via Google colab, il faut maintenant aussi changer quelques valeurs de configuration du réseau darknet. Pour cela il faut accéder au fichier makefile et lui modifier quelques entrées (GPU, CUDNN, CUDNN_HALF). On va aussi en profiter pour activer OpenCV et LIBSO (afin de récupérer les librairies nécessaires plus tard).

Bien sur on peut modifier directement le fichier, mais quand vous ré-ouvrirez le notebook, il faudra recommencer l’opération manuelle. Je vous propose à la place d’automatiser cela via l’utilisation de script sed comme ceci (à placer directement dans la cellule suivante du notebook) :

%cd darknet
!sed -i 's/OPENCV=0/OPENCV=1/' Makefile
!sed -i 's/GPU=0/GPU=1/' Makefile
!sed -i 's/CUDNN=0/CUDNN=1/' Makefile
!sed -i 's/CUDNN_HALF=0/CUDNN_HALF=1/' Makefile
!sed -i 's/LIBSO=0/LIBSO=1/' Makefile

Il ne vous reste plus qu’à compiler le réseau :

!make
mkdir -p ./obj/
mkdir -p backup
chmod +x *.sh
g++ -std=c++11 -std=c++11 -Iinclude/ -I3rdparty/stb/include -DOPENCV `pkg-config --cflags opencv4 2> /dev/null || pkg-config --cflags opencv` -DGPU -I/usr/local/cuda/include/ -DCUDNN -DCUDNN_HALF -Wall -Wfatal-errors -Wno-unused-result -Wno-unknown-pragmas -fPIC -Ofast -DOPENCV -DGPU -DCUDNN -I/usr/local/cudnn/include -DCUDNN_HALF -fPIC -c ./src/image_opencv.cpp -o obj/image_opencv.o
./src/image_opencv.cpp: In function ‘void draw_detections_cv_v3(void**, detection*, int, float, char**, image**, int, int)’:
./src/image_opencv.cpp:926:23: warning: variable ‘rgb’ set but not used [-Wunused-but-set-variable]
...

Vous allez voir que cela prend un peu de temps d’une part et que d’autre part vous aurez beaucoup de Warning … pas de panique, cela ne pose pas de soucis.

Le réseau est maintenant compilé et pret à être utilisé, il faut maintenant récupérer les poids car nous allons bien sur récupérer un réseau pré-entrainé :

!wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights
--2021-05-04 07:26:27--  https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights
Resolving github.com (github.com)... 140.82.121.3
Connecting to github.com (github.com)|140.82.121.3|:443... connected.
HTTP request sent, awaiting response... 302 Found
...
Saving to: ‘yolov4.weights’

yolov4.weights      100%[===================>] 245.78M  58.5MB/s    in 6.5s    

2021-05-04 07:26:33 (37.6 MB/s) - ‘yolov4.weights’ saved [257717640/257717640]

Cela doit aussi prendre un peu de temps selon votre connexion … remarquez que le fichier est plutot gros … c’est dire que le réseau YOLO est profond !

Premier essai en ligne de commande

Notre réseau de neurones est prêt à être utilisé. Nous allons maintenant le tester pour voir comment il fonctionne. Pour cela on va utiliser des images qui sont dans le répertoire data du reseau (vous pouvez aussi bien sur importer et tester avec les votres).

Voici la photo que l’on va utiliser (dog.jpg):

Lançons via la ligne de commande (dans une cellule colab). La syntaxe est plutot simple et nécessite le fichier de configuration du réseau (cfg), les poids et bien sur l’image source:

!./darknet detector test cfg/coco.data cfg/yolov4.cfg yolov4.weights data/dog.jpg
CUDA-version: 11000 (11020), cuDNN: 7.6.5, CUDNN_HALF=1, GPU count: 1  
 CUDNN_HALF=1 
 OpenCV version: 3.2.0
 0 : compute_capability = 370, cudnn_half = 0, GPU: Tesla K80 
net.optimized_memory = 0 
mini_batch = 1, batch = 8, time_steps = 1, train = 0 
   layer   filters  size/strd(dil)      input                output
   0 Create CUDA-stream - 0 
 Create cudnn-handle 0 
conv     32       3 x 3/ 1    608 x 608 x   3 ->  608 x 608 x  32 0.639 BF
   1 conv     64       3 x 3/ 2    608 x 608 x  32 ->  304 x 304 x  64 3.407 BF
   2 conv     64       1 x 1/ 1    304 x 304 x  64 ->  304 x 304 x  64 0.757 BF
   3 route  1 		                           ->  304 x 304 x  64 
   4 conv     64       1 x 1/ 1    304 x 304 x  64 ->  304 x 304 x  64 0.757 BF
   5 conv     32       1 x 1/ 1    304 x 304 x  64 ->  304 x 304 x  32 0.379 BF
   6 conv     64       3 x 3/ 1    304 x 304 x  32 ->  304 x 304 x  64 3.407 BF
...
 160 conv    255       1 x 1/ 1     19 x  19 x1024 ->   19 x  19 x 255 0.189 BF
 161 yolo
[yolo] params: iou loss: ciou (4), iou_norm: 0.07, obj_norm: 1.00, cls_norm: 1.00, delta_norm: 1.00, scale_x_y: 1.05
nms_kind: greedynms (1), beta = 0.600000 
Total BFLOPS 128.459 
avg_outputs = 1068395 
 Allocate additional workspace_size = 6.65 MB 
Loading weights from yolov4.weights...
 seen 64, trained: 32032 K-images (500 Kilo-batches_64) 
Done! Loaded 162 layers from weights-file 
 Detection layer: 139 - type = 28 
 Detection layer: 150 - type = 28 
 Detection layer: 161 - type = 28 
data/dog.jpg: Predicted in 174.085000 milli-seconds.
bicycle: 92%
dog: 98%
truck: 92%
pottedplant: 33%

Une fois de plus la trace est plutot verbeuse, mais si vous regardez les dernières lignes on peut trouver quelques informations intéressantes. On trouve n effet les différentes classes (objets) qui on été détectés avec leur probabilité (confiance dans la détection). AInsi on a bien sur cette photo un vélo, un chien, un truck … et une plante en pot (plottedplant) ??? Tiens c’est bizarre, on va regarder ça de plus près, d’autant que le score est de 33% (on a certaintement plutot un objet qui ressemble plus ou moins à une plante en pot en fait).

Par défaut, la ligne de commande créé un fichier (predictions.jpg) avec pour chaque objet un cadre. Nous allons l’afficher via Python dans colab :

import matplotlib.pyplot as plt
from skimage.io import imread, imshow

image = 'data/dog.jpg'
def display(_image):
  img1 = imread(_image)
  img2 = imread('predictions.jpg')
  fig, axes = plt.subplots(ncols=2)
  fig.set_size_inches(18.5, 10.5)
  axes[0].set_axis_off()
  axes[0].imshow(img1)
  axes[1].set_axis_off()
  axes[1].imshow(img2)
  plt.tight_layout()

display(image)

Regardez au fond à droite … il semblerait que notre réseau à confondu (à 33% ce qui est plutôt pas mal en fait) une poubelle avec une plante en pot.

Notre ligne de commande propose plusieurs options, en voici quelques unes :

Reglage du seuil de détection: Thresold [-thresh] afin de ne rapporter que les objets détectés au dessus d’un certain seuil:

!./darknet detector test cfg/coco.data cfg/yolov4.cfg yolov4.weights data/dog.jpg -thresh 0.5

Ne pas renvoyer l’image en sortie via l’option [-dont_show]

!./darknet detector test cfg/coco.data cfg/yolov4.cfg yolov4.weights data/person.jpg -dont_show

Pour renvoyer les coordonnées et informations au format texte [-ext_output]

Il y a beaucoup d’autres options disponibles sur github

Lancement via Python

Nous avons utilisé le binaire pour lancer la détection mais nous pouvons aussi utiliser Python via le fichier darknet_images.py

!python darknet_images.py --weights yolov4.weights --input data/dog.jpg

Maintenant si on veut incorporer l’utilisation de ce réseau dans un programme Python il existe plusieurs wrapper dans pyPI mais on peut aussi utiliser les fichiers Python fournis par darknet :

from darknet_images import image_detection
from darknet import load_network
import cv2

weights = 'yolov4.weights' 
input = 'data/dog.jpg'
datafile = './cfg/coco.data'
cfg = './cfg/yolov4.cfg'
thresh= 0.5

network, class_names, class_colors = load_network(cfg, datafile,  weights, 1)

image, detections = image_detection(
                                    input, 
                                    network, 
                                    class_names, 
                                    class_colors, 
                                    thresh
                                    )

Les deux premières lignes importent les fonctions préconstruites dans darknet (dans les fichiers darknet.py et darknet_images.py), ensuite on utilise directement ces fonctions. Le résultat est une image (matrice) et un objet python detection qui fournit les informations de détection:

Regardons le résultat:

detections
[('truck',
  '91.69',
  (454.5706787109375,
   130.06553649902344,
   174.90374755859375,
   98.58089447021484)),
 ('bicycle',
  '92.23',
  (271.85552978515625,
   292.2611389160156,
   362.61639404296875,
   315.614990234375)),
 ('dog',
  '97.89',
  (174.8762969970703,
   404.4398193359375,
   146.10897827148438,
   334.0611267089844))]

Nous retrouvons les 3 objets détectés avec leur probabilité et coordonnées.

Regardons l’image:

fig = plt.gcf()
fig.set_size_inches(18, 10)
plt.axis("off")
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))

Conclusion

Dans cet article je n’ai pas voulu allez ni dans les détails sur les concepts de YOLO/Darknet ni dans toutes les possibilités de que cette implémentation de YOLO peut offrir. L’idée était de montrer comment utiliser simplement ce réseau et surtout de donner un point de départ à l’utilisation de ce type de réseau. Une chose est claire, YOLO est rapide et pour l’avoir testé sur pas mal de photos le niveau de confiance est vraiment correct … maintenant comme toujours dans les réseaux de neurones il y a vraiment beaucoup (voire trop) de moyens pour le paramétrer mais aussi l’adapter à des détections spécifiques … un prochain article certainement ?

Les sources du notebook dans mon Github

N’hésitez pas à me faire part de vos commentaires

Partager cet article

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.

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