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.
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 :
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 😉
Pingback: YOLO (Partie 1) Introduction à YOLO avec Darknet - datacorner par Benoit Cayla
Pingback: YOLO (Partie 2) Utilisez YOLO avec OpenCV - datacorner par Benoit Cayla
Pingback: YOLO (Partie 4) Réduire le scope de détection - datacorner par Benoit Cayla
Pingback: YOLO (Partie 5) Créer son modèle avec YOLO : Préparation des données - datacorner par Benoit Cayla
Pingback: YOLO (Partie 6) Créer son modèle avec YOLO : Préparer le modèle - datacorner par Benoit Cayla