L’objectif de ce billet est d’expliquer la segmentation d’objets en temps réel par l’exemple. Pour cela, nous allons développer une application de segmentation en temps réel avec une webcam simple (embarquée dans le PC, USB ou autre). Nous utiliserons le framework Tensorflow, le réseau Mask RCNN ResNet101 appris avec le dataset COCO; ce qui nous permet de détecter jusqu’à 90 types d’objets différents.
Dans une première partie, je vais poser les grandes définitions et expliquer les choix de ce petit projet : pourquoi tensorflow, mask RCNN ResNet101 (what?), 90 objets mais lesquels, etc. La seconde partie est dédiée à la mise en place de l’environnement de travail, du téléchargement du modèle à l’installation des librairies utiles. Dans un troisième partie, nous créerons ensemble le code étape par étape en expliquant les points clés. Enfin pour faire plaisir à tous les profs de Français, je terminerai par une conclusion.
Le code est disponible sur ma page Github.
Définitions
La détection d’objets est un domaine très actif de la recherche qui cherche à classer et localiser des régions/zones d’une image ou d’un flux vidéo. Ce domaine est à la croisée de deux autres : la classification d’image et la localisation d’objets. En effet, le principe de la détection d’objets est le suivant : pour une image donnée, on recherche les régions de celle-ci qui pourraient contenir un objet puis pour chacune de ces régions découvertes, on l’extrait et on la classe à l’aide d’un modèle de classification d’image – par exemple – . Les régions de l’image d’origine ayant de bons résultats de classification sont conservés et les autres jetés. Ainsi, pour avoir une bonne méthode de détection d’objets, il est nécessaire d’avoir un algo solide de détection de régions et un bon algo de classification d’images.
La clef de la réussite est détenue dans l’algorithme de classification d’image. Depuis les résultats du challenge ImageNet 2012, le deep learning (et notamment les réseaux de convolution) est devenue la méthode number #1 pour résoudre ce genre de problème. Le recherches en détection d’objets ont donc tout naturellement intégré les modèles de classification d’image, ce qui a permis de créer des bijoux tels que SSD et R-CNN (le R signifiant ici Region). L’image #1 illustre le résultat d’une détection d’objets (de voitures en l’occurrence). On observe que plusieurs objets peuvent être découverts et localisés dans une même image.
SSD et R-CNN ne sont jamais utilisés seuls, ils sont couplés avec un réseau pré-calculé de classification d’image. Un réseau auquel on a calculé les poids est appelé un modèle. Il existe ainsi des SSD Mobilenet, des R-CNN Inception, etc.
Malgré tout, il manque quelque chose dans cette équation : avec quoi les poids des réseaux de classification d’image ont été calculé ? La réponse est : un gros dataset d’images. Généralement c’est avec le dataset MSCOCO (souvent réduit à COCO) qui est utilisé. MSCOCO est un ensemble de plus de 330k images contenant des dizaines de types d’objets différents. Je vous invite à fouiller le site officiel pour en savoir davantage.
Mais soyons plus exigeants et demandons à notre IA d’être précise au pixel près. Répondre à cette question, c’est faire de la segmentation d’objets (instance segmentation).
Pour ce genre de tâche le principe reste globalement le même, simplement, à la fin de notre réseau est ajouté une étape de création de masque capable de dire si oui ou non un pixel fait parti de notre objet détecté.
Les réseaux de segmentation d’objets commencent en général par le mot clé “Mask”. Tensorflow propose par exemple 4 modèles de segmentation. Celui que nous utiliserons dans ce tutoriel est mask_rcnn_resnet101_atrous_coco. L’image #2 illustre le résultat d’un modèle de segmentation d’objets.
Enfin, les modèles de segmentation d’objets et de détection d’objets sont évalués non pas en fonction de leur précision/accuracy (à l’instar des modèles de classification d’image) mais à l’aide d’une mesure de calcul de recouvrement : le mAP (Mean Average Precision). L’objectif de cette mesure est de comparer la région détectée avec la vraie région de l’objet. Un article entier est consacré à cette mesure.
J’ai présenté les informations essentielles de la détection et la segmentation d’objets. Je ne suis pas entré dans les détails des réseaux mais beaucoup d’autres articles le font bien. Je vous invite à fouiller scholar.google ou tout simplement google pour en connaître davantage. Dans la partie suivante, je vais parler de la configuration nécessaire pour faire tourner notre programme de segmentation d’objets en temps réel.
Récupération des outils (+ détails des versions)
Avant toute choses, faisons la liste de nos outils et librairies à installer.
- Linux Ubuntu 18.04. Tout ce qui suit a été testé avec Ubuntu 18.04. Que vous soyez sur Mac ou Windows, le programme fonctionnera, seuls les indications des installations ne sont pas compatibles.
- Python. Nous utiliserons le langage de programmation python dans sa version 3, faites donc en sorte d’installer cette version de python (python3.6 pour être précis). Il est très probable que le programme tourne avec une autre version de python, mais je ne garantis rien.
- Tensorflow. Il évidemment nécessaire d’installer tensorflow (tensorflow-gpu si votre PC est muni d’un GPU compatible). Si pip3 est installé, rien de plus simple avec la commande pip3 install tensorflow.
- Tensorflow object detection. On aura besoin du module object detection de tensorflow. Note : si vous faites l’export du pythonpath dans un terminal, conservez bien ce terminal pour exécuter le programme de segmentation sinon la modification du pythonpath sera perdue. Vous pouvez aussi l’ajouter au bashrc également.
- Open CV. La gestion du flux vidéo est géré par la célèbre libraire opencv. Pour l’installer, entrer dans votre terminal pip3 install opencv-python.
Pour tester si votre système est prêt, ouvrez une console python3 dans le terminal en tapant python3. Faites les imports suivant :
import cv2 import numpy as np import tensorflow as tf from object_detection.utils import label_map_util from object_detection.utils import ops as utils_ops from object_detection.utils import visualization_utils as vis_util
Si il y a une erreur, alors vérifiez vos installations.
Pour faire fonctionner le modèle, il faut… le réseau et ses poids. Vous trouverez la liste de tous les modèles d’object detection disponibles avec tensorflow sur le model zoo. Pour ce tutoriel, téléchargez mask_rcnn_resnet101_atrous_coco.
Développement étape par étape
1 – Initialisation du modèle avec tensorflow
La première étape de notre programme est l’initialisation du modèle de tensorflow. Il s’agit ici d’importer le graphe du modèle associé à ses poids pré-calculés à l’aide du fichier frozen_inference_graphe.pb, de récupérer les tenseurs de sorties et celui d’entrée, d’importer les labels et d’initialiser la capture vidéo de la webcam.
On commence d’ailleurs par le dernier point : initialiser la webcam. Cela permet deux choses : (1) créer le flux vidéo et (2) obtenir la largeur et la hauteur d’une frame du flux dans le but de configurer le tenseur de sortie du masque.
# Init the video stream (with the first plugged webcam) cap = cv2.VideoCapture(0) if cap.isOpened(): # get vcap property global_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) global_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
Rien de compliqué ici. Si vous souhaitez gérer le flux vidéo dans un Thread à part, je vous conseille de regarder du côté de la librairie imutils.
La suite concerne l’importation du fichier label_map. Elle se réalise à l’aide des méthodes délivrées avec object_detection de tensorflow.
label_map = label_map_util.load_labelmap("PATH/TO/LABELS") categories = label_map_util.convert_label_map_to_categories( label_map, max_num_classes=NUM_CLASSES, use_display_name=True) category_index = label_map_util.create_category_index(categories)
On poursuit ensuite avec l’importation du modèle et la récupération des tenseurs.
# Init TF Graph and get all needed tensors detection_graph = tf.Graph() with detection_graph.as_default(): # Init the graph od_graph_def = tf.GraphDef() with tf.gfile.GFile("/chemin/vers/*.pbb') as fid: serialized_graph = fid.read() od_graph_def.ParseFromString(serialized_graph) tf.import_graph_def(od_graph_def, name='') # Get all tensors ops = tf.get_default_graph().get_operations() all_tensor_names = {output.name for op in ops for output in op.outputs} tensor_dict = {} for key in ['num_detections', 'detection_boxes', 'detection_scores', 'detection_classes', 'detection_masks']: tensor_name = key + ':0' if tensor_name in all_tensor_names: tensor_dict[key] = tf.get_default_graph().get_tensor_by_name(tensor_name) # detection_masks tensor need ops detection_boxes = tf.squeeze(tensor_dict['detection_boxes'], [0]) detection_masks = tf.squeeze(tensor_dict['detection_masks'], [0]) # Reframe is required to translate mask from box coordinates to image coordinates and fit the image size. real_num_detection = tf.cast(tensor_dict['num_detections'][0], tf.int32) detection_boxes = tf.slice(detection_boxes, [0, 0], [real_num_detection, -1]) detection_masks = tf.slice(detection_masks, [0, 0, 0], [real_num_detection, -1, -1]) detection_masks_reframed = utils_ops.reframe_box_masks_to_image_masks( detection_masks, detection_boxes, global_height, global_width) detection_masks_reframed = tf.cast(tf.greater(detection_masks_reframed, 0.5), tf.uint8) # Follow the convention by adding back the batch dimension tensor_dict['detection_masks'] = tf.expand_dims(detection_masks_reframed, 0) image_tensor = tf.get_default_graph().get_tensor_by_name('image_tensor:0')
L’importation est classique, il n’y a rien de spécifique à expliquer. Concernant les tenseurs de sortie, on récupère ‘num_detections‘, ‘detection_boxes‘, ‘detection_scores‘, ‘detection_classes‘, ‘detection_masks‘ et, ‘detection_masks‘. On notera qu’on se fout un peu de ‘num_detections’ mais ce tenseur est appelé dans la doc officielle, alors moi aussi je l’appelle.
Poursuivons notre programme avec la méthode main. Dans cette méthode qui est appelé après les initialisations que je viens de présenter, on capture la dernière image de la webcam, on la traite, on affiche l’image modifiée et on recommence. La méthode main ressemble donc à :
if __name__ == '__main__':
# Do that here and save a lot of time
with detection_graph.as_default():
with tf.Session(graph=detection_graph) as sess:
while True:
# Get the last frame
frame = cap.read()[1]
# Process last img
new_frame = detect_objects(image_np, sess)
# Display the resulting frame
cv2.imshow('new_frame', new_frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
On commence notre code avec une série de deux blocs with (en réalité on peut n’en faire qu’un mais c’est plus joli et plus propre avec deux). Ces blocs permettent de définir la session tensorflow. S’en suit une boucle infinie qui nous permet de boucler sur le flux vidéo. À chaque étape de cette boucle, on récupère la dernière image du buffer du flux de la caméra avec la commande cap.read puis on fait un appelle à la méthode detect_objects() que je vais présenter juste après. On finit en affichant la frame modifiée avec cv2.imshow.
Et donc le cœur de programme se trouve dans la méthode detect_objects() qui prend en arguments l’image (au format numpy array) et la session tensorflow qui nous permettra de runner notre modèle.
Le code est le suivant :
def detect_objects(image_np, sess): # Run inference output_dict = sess.run(tensor_dict, feed_dict={image_tensor: np.expand_dims(image_np, 0)}) # all outputs are float32 numpy arrays, so convert types as appropriate output_dict['num_detections'] = int(output_dict['num_detections'][0]) output_dict['detection_classes'] = output_dict['detection_classes'][0].astype(np.uint8) output_dict['detection_boxes'] = output_dict['detection_boxes'][0] output_dict['detection_scores'] = output_dict['detection_scores'][0] output_dict['detection_masks'] = output_dict['detection_masks'][0] # Display boxes and color pixels vis_util.visualize_boxes_and_labels_on_image_array( image_np, output_dict['detection_boxes'], output_dict['detection_classes'], output_dict['detection_scores'], category_index, instance_masks=output_dict.get('detection_masks use_normalized_coordinates=True) return image_np
Comme on le voit, tensorflow s’occupe de tout, de l’appel des bons tenseurs à la coloration des pixels de l’image en fonction des résultats de l’object detection. La première étape consiste à passer notre image dans le réseau et de récupérer les résultats. C’est exactement ce qu’il se passe avec la ligne sess.run. Ensuite, les résultats sont mis au bon formats afin d’être traiter par le script du module d’object detection permettant la visualisation des résultats. Enfin, on appelle la méthode visualize_boxes_and_labels_on_image_array qui se trouve dans le fichier object_detection/utils:visualization_utils.py. Si vous souhaitez modifier l’apparence des résultats, c’est donc ce fichier qu’il faudra modifier.
Conclusion
Nous avons développé un programme de segmentation et détection d’objets en temps réel en python. Pour cela, on s’est appuyé sur le réseau Mask RCNN ResNet101 implémenté par tensorflow et qui a été entraîné avec le dataset MSCOCO. Le code complet est disponible sur Github par ici. N’hésitez pas si vous avez la moindre question ou remarque.