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.
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.
5 Replies to “YOLO (Partie 3) Non Maxima Suppression (NMS)”