YOLO (Partie 3) Non Maxima Suppression (NMS) 5   Mise à jour récente !


Dans les articles précédents sur YOLO nous avons vu comment utiliser ce réseau … mais quand on applique cet algorithme sur des images complexes on constate vite que de multiples détections sont faites pour les mêmes objets. Nous allons voir dans cet article comment supprimer ces cadres doublons avec la technique dite de NMS.

Pour cet article il vous faudra juste une connexion internet et un compte Google car nous allons utiliser google Colab. Coté connaissances, Python est un must mais rassurez-vous, pas besoin d’être un expert pour suivre 😉

Qu’est-ce que le NMS ?

Le soucis avec YOLO et les objets que l’algorithme doit détecter c’est qu’ils peuvent être de différentes tailles et formes. Pour les capturer chacun d’eux, les algorithmes de détection d’objets tels que YOLO créent alors plusieurs cadres de délimitation. Bien sur, pour chaque objet a détecter, nous n’avons besoin que d’un seul cadre de délimitation. Il nous faut donc retirer les détections en doublons.

L’objectif du NMS est plutôt simple : Il faut « supprimer » les cadres de délimitation les moins probables et ne conserver que le meilleur.

Principe du NMS

Le but de l’algorithme de NMS est donc de sélectionner le meilleur cadre de détection pour un même objet et ainsi de retirer tous les autres cadres.

Pour cela Le NMS prend en compte deux critères de qualité:

  • Le score d’objectivité donné par le modèle
  • Le chevauchement des cadres


Vous pouvez voir l’image ci-dessous, ainsi que les cadres de délimitation de chacune des boites détectées:

YOLO renvoi aussi un score d’objectivité lors de la détection pour chaque cadre. Ce score indique à quel point le modèle est certain que l’objet souhaité est présent dans cette boîte englobante.

L’algorithme NMS va donc sélectionner le cadre avec le score d’objectivité le plus élevé, ensuite il retirera tous les autres cadres qui ont un chevauchement important avec le cadre choisit. Dans le principe c’est finalement assez simple 😉

Voyons où est le problème contrêtement

On va repartir de l’exemple que l’on avait traité dans l’article précédent. Le code ci-dessous ne fait donc que reprendre de manière synthétique ce que j’avais déjà présenté précédemment. Notez juste la création et le remplissage des tableaux confidences_scores, confidences_scores et labels_detected. Si ils n’étaient pas indispensables précédemment ils vont justement servir à l’application du NMS plus tard.

Python
YOLO (Partie 3) Non Maxima Suppression (NMS)

    Introduction à YOLO avec Darknet
    YOLO avec OpenCV
    YOLO & Non Maxima Suppression (NMS)
    Réduire le scope de détection (Nombre d'objets)
    Créer son modèle avec YOLO : Préparation des données
    Créer son modèle avec YOLO : Préparer le modèle
    Créer son modèle avec YOLO : Entraînement et résultat

Dans les articles précédents sur YOLO nous avons vu comment utiliser ce réseau … mais quand on applique cet algorithme sur des images complexes on constate vite que de multiples détections sont faites pour les mêmes objets. Nous allons voir dans cet article comment supprimer ces cadres doublons avec la technique dite de NMS.

Pour cet article il vous faudra juste une connexion internet et un compte Google car nous allons utiliser google Colab. Coté connaissances, Python est un must mais rassurez-vous, pas besoin d'être un expert pour suivre ;-)
Qu'est-ce que le NMS ?

Le soucis avec YOLO et les objets que l'algorithme doit détecter c'est qu'ils peuvent être de différentes tailles et formes. Pour les capturer chacun d'eux, les algorithmes de détection d'objets tels que YOLO créent alors plusieurs cadres de délimitation. Bien sur, pour chaque objet a détecter, nous n'avons besoin que d'un seul cadre de délimitation. Il nous faut donc retirer les détections en doublons.

L'objectif du NMS est plutôt simple : Il faut « supprimer » les cadres de délimitation les moins probables et ne conserver que le meilleur.
Principe du NMS

Le but de l'algorithme de NMS est donc de sélectionner le meilleur cadre de détection pour un même objet et ainsi de retirer tous les autres cadres.

Pour cela Le NMS prend en compte deux critères de qualité:

    Le score d'objectivité donné par le modèle
    Le chevauchement des cadres 


Vous pouvez voir l'image ci-dessous, ainsi que les cadres de délimitation de chacune des boites détectées:
L’attribut alt de cette image est vide, son nom de fichier est yolo_3_1.jpg.

YOLO renvoi aussi un score d'objectivité lors de la détection pour chaque cadre. Ce score indique à quel point le modèle est certain que l'objet souhaité est présent dans cette boîte englobante.

L'algorithme NMS va donc sélectionner le cadre avec le score d'objectivité le plus élevé, ensuite il retirera tous les autres cadres qui ont un chevauchement important avec le cadre choisit. Dans le principe c'est finalement assez simple ;-)
Voyons où est le problème contrêtement

On va repartir de l'exemple que l'on avait traité dans l'article précédent. Le code ci-dessous ne fait donc que reprendre de manière synthétique ce que j'avais déjà présenté précédemment. Notez juste la création et le remplissage des tableaux confidences_scores, confidences_scores et labels_detected. Si ils n'étaient pas indispensables précédemment ils vont justement servir à l'application du NMS plus tard.

Votre site ne prend pas en charge le bloc SyntaxHighlighter Evolved. Vous pouvez essayer de l’installer, de le convertir en bloc de HTML personnalisé ou de le retirer complètement.
Installer SyntaxHighlighter EvolvedConserver en HTML

import numpy as np
import cv2
from google.colab.patches import cv2_imshow # colab do not support cv2.imshow()
ROOT_COLAB = '/content/drive/MyDrive/Colab Notebooks/YOLO'
YOLO_CONFIG = ROOT_COLAB + '/oc_data/'
COCO_LABELS_FILE = YOLO_CONFIG + 'coco.names'
YOLO_CONFIG_FILE = YOLO_CONFIG + 'yolov4.cfg'
YOLO_WEIGHTS_FILE = YOLO_CONFIG + 'yolov4.weights'
LABELS_FROM_FILE = False
IMAGE_FILE = 'yoloimg.jpg'
IMAGE = cv2.imread(ROOT_COLAB + '/' + IMAGE_FILE)
CONFIDENCE_MIN = 0.5
# Little function to resize in keeping the format ratio
# Source: https://stackoverflow.com/questions/35180764/opencv-python-image-too-big-to-display
def ResizeWithAspectRatio(_image, width=None, height=None, inter=cv2.INTER_AREA):
    dim = None
    image = _image.copy()
    (h, w) = image.shape[:2]
    if width is None and height is None:
        return image
    if width is None:
        r = height / float(h)
        dim = (int(w * r), height)
    else:
        r = width / float(w)
        dim = (width, int(h * r))
    return cv2.resize(image, dim, interpolation=inter)
with open(COCO_LABELS_FILE, 'rt') as f:
    labels = f.read().rstrip('\n').split('\n')
np.random.seed(45)
BOX_COLORS = np.random.randint(0, 255, size=(len(labels), 3), dtype="uint8")
yolo = cv2.dnn.readNetFromDarknet(YOLO_CONFIG_FILE, YOLO_WEIGHTS_FILE)
yololayers = [yolo.getLayerNames()[i[0] - 1] for i in yolo.getUnconnectedOutLayers()]
blobimage = cv2.dnn.blobFromImage(IMAGE, 1 / 255.0, (416, 416),	swapRB=True, crop=False)
yolo.setInput(blobimage)
layerOutputs = yolo.forward(yololayers)
boxes_detected = []
confidences_scores = []
labels_detected = []
# loop over each of the layer outputs
for output in layerOutputs:
  # loop over each of the detections
  for detection in output:
    # extract the class ID and confidence (i.e., probability) of the current object detection
    scores = detection[5:]
    classID = np.argmax(scores)
    confidence = scores[classID]
    
    # Take only predictions with confidence more than CONFIDENCE_MIN thresold
    if confidence > CONFIDENCE_MIN:
      # Bounding box
      box = detection[0:4] * np.array([W, H, W, H])
      (centerX, centerY, width, height) = box.astype("int")
      # Use the center (x, y)-coordinates to derive the top and left corner of the bounding box
      x = int(centerX - (width / 2))
      y = int(centerY - (height / 2))
      # update our result list (detection)
      boxes_detected.append([x, y, int(width), int(height)])
      confidences_scores.append(float(confidence))
      labels_detected.append(classID)

On va maintenant afficher le résultat brut :

Python
image = IMAGE.copy()
if nb_results > 0:
  for i in range(nb_results):
    # extract the bounding box coordinates
    (x, y) = (boxes_detected[i][0], boxes_detected[i][1])
    (w, h) = (boxes_detected[i][2], boxes_detected[i][3])
    # draw a bounding box rectangle and label on the image
    color = [int(c) for c in BOX_COLORS[labels_detected[i]]]
    cv2.rectangle(image, (x, y), (x + w, y + h), color, 1)
    score = str(round(float(confidences_scores[i]) * 100, 1)) + "%"
    text = "{}: {}".format(labels[labels_detected[i]], score)
    cv2.putText(image, text, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
  
cv2_imshow(ResizeWithAspectRatio(image, width=700))

Voyez les multiples boites (englobées entres elles) qui montrent les multiples détections pour le même objet, cette superposition est même assez désagréable car on ne peut même pas lire les libellés.

Si on regarde de plus près le tableaux label_names, rien d’étonnant à ça :

Python
label_names = [labels[i] for i in labels_detected]
label_names
['cell phone',
'cell phone',
'cell phone',
'cell phone',
'cell phone',
'cell phone',
'cell phone',
'laptop',
'laptop',
'laptop',
'cell phone']

NMS en action !

Excellente nouvelle nous n’aurons pas à coder la fonction NMS, à la place OpenCV en fournit une prête à l’emploi cv2.dnn.NMSBoxes 🙂

En fait ici on comprend l’importance d’avoir créé et remplit les tableaux boxes_detected et confidences_scores car ils vont être utilisés par la fonction NMS directement pour filtrer les détection en double:

Python
final_boxes = cv2.dnn.NMSBoxes(boxes_detected, confidences_scores, 0.5, 0.5)

Regardons le résultat:

Python
image = IMAGE.copy()
# loop through the final set of detections remaining after NMS and draw bounding box and write text
for max_valueid in final_boxes:
    max_class_id = max_valueid[0]
    # extract the bounding box coordinates
    (x, y) = (boxes_detected[max_class_id][0], boxes_detected[max_class_id][1])
    (w, h) = (boxes_detected[max_class_id][2], boxes_detected[max_class_id][3])
    # draw a bounding box rectangle and label on the image
    color = [int(c) for c in BOX_COLORS[labels_detected[max_class_id]]]
    cv2.rectangle(image, (x, y), (x + w, y + h), color, 1)
    
    score = str(round(float(confidences_scores[max_class_id]) * 100, 1)) + "%"
    text = "{}: {}".format(labels[labels_detected[max_class_id]], score)
    cv2.putText(image, text, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
cv2_imshow(ResizeWithAspectRatio(image, width=700))

Voilà le résultat, et ce rien qu’en ajoutant 1 seule ligne … magique non ?

Une simple ligne mais qui a son importance. Sans ça vous pourriez vous retrouver avec de multiples objets détectés (pleins de faux positifs) … imaginez sur une photo avec pleins d’objets ce que ça pourrait donner 😉

Retrouvez le notebook sur Github.

Lire la suite

Dans l’article suivant nous verrons comment réduire simplement le nombre d’objets détectés.


A propos de Benoit Cayla

Ingénieur en informatique avec plus de 20 ans d’expérience dans la gestion et l’utilisation de données, Benoit CAYLA a mis son expertise au profit de projets très variés tels que l’intégration, la gouvernance, l’analyse, l’IA, la mise en place de MDM ou de solution PIM pour le compte de diverses entreprises spécialisées dans la donnée (dont IBM, Informatica et Tableau). Ces riches expériences l’ont naturellement conduit à intervenir dans des projets de plus grande envergure autour de la gestion et de la valorisation des données, et ce principalement dans des secteurs d’activités tels que l’industrie, la grande distribution, l’assurance et la finance. Également, passionné d’IA (Machine Learning, NLP et Deep Learning), l’auteur a rejoint Blue Prism en 2019 et travaille aujourd’hui en tant qu’expert data/IA et processus. Son sens pédagogique ainsi que son expertise l’ont aussi amené à animer un blog en français (datacorner.fr) ayant pour but de montrer comment comprendre, analyser et utiliser ses données le plus simplement possible.

Laissez un commentaire

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

5 commentaires sur “YOLO (Partie 3) Non Maxima Suppression (NMS)