Optimisation de notre ligne de performance
La théorie
Ok, posons déjà un peu le schéma de ce qu’on veut faire. Ce sera plus simple ensuite pour faire le code.

A chaque boucle, lorsque l’on va appeler notre méthode textPerf
, on va calculer le temps écoulé, ajouter 1 à notre compteur de boucle/image. Ensuite, on vérifie si on a atteint 1 seconde au total écoulé. Si c’est le cas, on peut alors calculer le temps écoulé moyen pour le nombre de boucle effectuée. Dans l’image ci-dessus, il y a eu 3 boucles pendant 1,1 seconde. Donc, si on fait 1,1 seconde divisé par 3 boucles, ça nous donne une moyenne de 0,36 seconde par boucle. Pour calculer la fréquence moyenne, on divise 1 par 0,36 donc 2,72 boucles par seconde donc par extrapolation 2,72 images par seconde.
Le code python
Mettons tout ça sous forme de code. On va reprendre notre classe PerfImage
et on va l’améliorer un peu.
Au niveau des déclarations, ça ne change pas
## On importe les librairies dont on va avoir besoin
import psutil
import time
Dans la déclaration de notre classe, on va ajouter un argument. tempsEntre2perf
. En fait cet argument va permettre de définir tous les combien de temps nous allons faire nos calculs et mettre à jour notre ligne de perf.
class PerfImage:
def __init__(self,tempsEntre2perf):
Ensuite on déclare nos attributs. on connait déjà __tempsPrecedent
self.__tempsPrecedent = 0
Comme il nous faut calculer le temps écoulé pendant plusieurs boucles, nous avons besoin d’un attribut qui va stocker au fur et à mesure le temps total écoulé
self.__tempsEcoule = 0
De la même façon, il nous faut un attribut qui va stocker au fur et à mesure le nombre de boucle effectuée
self.__compteurImage = 0
On crée aussi un attribut qui va contenir le temps entre 2 performances
self.__tempsEntre2perf = tempsEntre2perf
Lorsque le temps écoulé n’a pas dépassé le temps que nous avons défini entre 2 performances, il faut quand même renvoyer une ligne de performance. Pour ça, nous allons utiliser un attribut. A chaque fois qu’on fera le calcul on mettra à jour cet attribut. Et le reste du temps, on ne renverra que la ligne précédente… en attendant le nouveau calcul 🙂
self.__texte = 'FPS: {:2.1f} - CPU: {:2.0f}% - MEM: {:2.2f}Go'.format(0,0,0)
Pour la partie qui suit, on ajoute notre compteur qui s’incrémente à chaque boucle
def textePerf(self):
## On ajoute 1 à notre compteur d'image affichée
self.__compteurImage = self.__compteurImage + 1
## On prend le temps exact à l'instant t et on le met dans notre variable
tempsActuel = time.time()
Désormais, il faut qu’on ajoute le temps écoulé depuis la dernière boucle au temps total écoulé.
## Petite sécurité histoire d'éviter la division par 0 lors du premier
## appel
if self.__tempsPrecedent != 0:
## On récupère le temps écoulé entre le temps actuel t et le temps lors
## du précédent appel
self.__tempsEcoule = self.__tempsEcoule + (tempsActuel - self.__tempsPrecedent)
else:
fps = 0
Est-ce qu’on a dépassé le temps entre 2 performances ?
if self.__tempsEcoule >= self.__tempsEntre2perf:
Si on a dépassé, on lance les calculs et on met à jour notre ligne de performance
## On calcule notre fréquence moyenne
fps = 1 / (self.__tempsEcoule / self.__compteurImage)
## on récupère le pourcentage d'utilisation du cpu
cpu = psutil.cpu_percent()
## On récupère la quantité de mémoire total utilisée
mem = (psutil.virtual_memory().used)/1024/1024/1024
## On prépare notre chaine de caractère contenant les infos
self.__texte = 'FPS: {:2.1f} - CPU: {:2.0f}% - MEM: {:2.2f}Go'.format(fps,cpu,mem)
On remet l’affiche du temps écoulé et du nombre de FPS dans la console
## Tu peux décommenter la ligne ci dessous si tu souhaites voir
## le temps écoulé entre 2 appels
print("Temps écoulé : {:2.2f} - FPS : {:2.0f}".format(self.__tempsEcoule, fps))
Il ne faut pas oublier de remettre à 0 le temps total écoulé et notre compteur de boucle
## On réinitialise nos attributs qui stockent le nombre d'image
## et le temps écoulé pour recommercer le process à la prochaine
## boucle
self.__compteurImage = 0
self.__tempsEcoule = 0
Afin de pouvoir calculer le temps écoulé entre 2 boucles, on affecte le temps actuel au temps précédent pour la prochaine boucle. Attention : Cette ligne ne doit pas être intégrée à notre condition IF au-dessus. Vérifie bien ton indentation.
## On en profite pour affecter le temps actuel à notre attribut
## __tempsPrecedent. Comme ça, ce sera prêt pour la prochaine
## exécution
self.__tempsPrecedent = tempsActuel
Et on renvoie notre ligne
## On retourne notre chaine de caractère là où ça a été demandé
return self.__texte
On sauvegarde parce que ce serait bête de devoir tout se retaper !
Dans notre script principal, il nous faut modifier l’instanciation de notre objet pour y ajouter notre argument tempsEntre2perf
.
## On instancie notre objet depuis la classe PerfiImage
monPerfImage = perfImage.PerfImage(1)
Le 1 correspond au fait qu’on veut que la fréquence moyenne soit calculée toutes les une seconde.
Si on exécute notre script, désormais l’affichage des FPS est plus stable et il y a déjà moins de variations de FPS. Voici la sortie de ma console
Temps écoulé : 1.00 - FPS : 29
Temps écoulé : 1.05 - FPS : 27
Temps écoulé : 1.00 - FPS : 29
Temps écoulé : 1.01 - FPS : 29
Temps écoulé : 1.05 - FPS : 30
Temps écoulé : 1.01 - FPS : 31
Temps écoulé : 1.00 - FPS : 30
Néanmoins, on sait que si on calcule la fréquence à chaque boucle, nous avons des variations. Un coup c’est 19 FPS et un autre 91 FPS. Ça veut dire qu’il y a quelque chose dans notre boucle qui prend beaucoup de temps quand on est à 19 FPS et très peu de temps quand on est à 91 FPS. D’après toi ?
Alors, histoire de savoir quelle partie du code, j’ai ajouté de l’affichage du temps écoulé entre chaque étape :
while True:
tempsActuel = time.time()
tempsPrecedent = tempsActuel
print ("Top départ")
##on récupère la dernière image de la vidéo
valeurRetour, imageWebcam = videoWebcam.read()
tempsPrecedent = tempsActuel
tempsActuel = time.time()
print ("récupération de l'image : {:2.4f}".format(tempsActuel-tempsPrecedent))
## on s'assurer qu'aucune erreur n'a été rencontrée
if valeurRetour:
## On récupère notre texte avec les performances
textePerf = monPerfImage.textePerf()
tempsPrecedent = tempsActuel
tempsActuel = time.time()
print ("récupération de la ligne de perf : {:2.4f}".format(tempsActuel-tempsPrecedent))
## On ajoute notre ligne à l'image
cv2.putText(imageWebcam, textePerf, (10, 15),
cv2.FONT_HERSHEY_SIMPLEX , 0.5, (0, 0, 0), thickness=2, lineType=1)
tempsPrecedent = tempsActuel
tempsActuel = time.time()
print ("Ajout de la ligne de perf à l'image : {:2.4f}".format(tempsActuel-tempsPrecedent))
## On affiche l'image
cv2.imshow('Image de la webcam', imageWebcam)
tempsPrecedent = tempsActuel
tempsActuel = time.time()
print ("Affichage de l'image : {:2.4f}".format(tempsActuel-tempsPrecedent))
## Comme c'est une boucle infinie, il faut bien se prévoir une sortie
## Dans notre cas, ce sera l'appui sur la touche Q
if cv2.waitKey(1) & 0xFF == ord('q'):
break
Et voici le résultat :
Top départ
récupération de l'image : 0.0658
récupération de la ligne de perf : 0.0000
Ajout de la ligne de perf à l'image : 0.0000
Affichage de l'image : 0.0000
Top départ
récupération de l'image : 0.0080
récupération de la ligne de perf : 0.0000
Ajout de la ligne de perf à l'image : 0.0000
Affichage de l'image : 0.0000
Top départ
récupération de l'image : 0.0499
récupération de la ligne de perf : 0.0000
Ajout de la ligne de perf à l'image : 0.0000
Affichage de l'image : 0.0000
Top départ
récupération de l'image : 0.0080
récupération de la ligne de perf : 0.0000
Ajout de la ligne de perf à l'image : 0.0000
Affichage de l'image : 0.0000
Il est clair désormais que c’est la récupération de l’image de la webcam qui ralentie notre boucle. C’est à dire cette ligne
valeurRetour, imageWebcam = videoWebcam.read()
Et c’est vraiment pénalisant car cela va fausser toutes nos prochaines comparaisons quand on jouera avec la détection et la reconnaissance faciale.
On comprend donc que la méthode read()
bloque l’exécution de notre script jusqu’à la récupération de l’image. Et cette récupération de l’image prend environ 50ms dans mon cas. Par contre, si jamais l’image n’a pas changé depuis la dernière boucle, alors là, la récupération de l’image se fait en 8 ms. N’oublie pas, ma webcam envoie 30 images par seconde. Mais la boucle s’exécute beaucoup plus vite. Donc elle va appeler à plusieurs reprises la méthode read()
alors que l’image de la webcam n’a pas changé !
Alors comment résoudre ce souci ?
Bonjour, j’ai essayé de rentrer le code mais lorsque je dois installer le module outils, je tape pip install outils, je redémarre le kernel mais après, spyder me dit:”No module named ‘outils'”. Quelqu’un saurait ce que je devrais faire?
Bonjour Jeremy,
Effectivement, je ne vois pas la trace du fichier outils.py dans l’article.
Essaye de remplacer
outils.perfImage as perfImage
parperfImage
Cela dépend de la où tu as mis la classe perfImage.
Ced