Niveau
Débutant
Env.
Google Colab
Code
Python
Libs
cv2, darknet, numpy
Sources

YOLO (Partie 3) Non Maxima Suppression (NMS)

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.

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 :

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 :

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:

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

Regardons le résultat:

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.

Partager cet article

5 Replies to “YOLO (Partie 3) Non Maxima Suppression (NMS)”

Laisser un commentaire

Votre adresse e-mail 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.