Reconnaissance faciale
Transcription
Reconnaissance faciale
Reconnaissance faciale Thibault Suzanne, Ralph Maloudi Table des matières Introduction 3 1 La reconnaissance faciale 1.1 Les faces propres : la théorie . . . . . . . . . . . . . . . . . . . . . 1.2 Les faces propres : l’implantation pratique . . . . . . . . . . . . . 4 4 9 2 L’interface graphique 15 2.1 Fonctionnement général . . . . . . . . . . . . . . . . . . . . . . . 15 2.2 Implantation python . . . . . . . . . . . . . . . . . . . . . . . . . 16 Conclusion 20 1 Table des figures 1.1 1.2 1.3 1.4 Illustration de l’ACP . . . . . . . . . . . . . . . . Approximation d’un créneau par des sinusoïdales Quelques faces propres . . . . . . . . . . . . . . . Illustration de la distance de Mahalanobis . . . . . . . . 5 5 7 9 2.1 Capture d’écran du logiciel (Arch Linux, gestionnaire de fenêtre Awesome) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introduction Ce projet a pour objectif la réalisation d’un logiciel de reconnaissance faciale. Il est découpé en deux parties liées mais distinctes : le codage d’un outil de reconnaissance faciale à l’aide de l’algorithme eigenfaces, et la réalisation d’une interface graphique permettant d’utiliser n’importe quel programme de reconnaissance pourvu qu’il respecte un certain format. En particulier, cette interface servira à l’utilisation de notre premier code. Le programme de reconnaissance faciale sera codé en Python. Il utilisera les bibliothèques OpenCV et Numpy. Son rôle sera d’identifier, parmi une base de visage connu, duquel se rapproche le plus un visage passé en paramètre. Il devra également indiquer à quel point il s’en rapproche, afin de pouvoir valider ou infirmer l’identification. Il utilisera la technique dite des « faces-propres », qui bien que n’offrant pas les meilleures performances à l’heure actuelle, est relativement simple à mettre en œuvre et reste efficace. L’interface graphique sera également réalisée en Python, à l’aide de la plateforme PySide, qui est un portage de la librairie Qt pour le langage Python. Elle utilisera également OpenCV pour pouvoir prendre des photos à l’aide de la webcam. Elle devra prendre une photo à l’aide de la webcam et permettre le choix d’un module de reconnaissance parmi ceux qu’elle connait (qui seront dans une liste facilement extensible). Elle devra ensuite appeler ce module afin qu’il reconnaisse ou non le visage en question. Elle affichera dans une fenêtre le visage reconnu par le module, ainsi que diverses informations que ledit module aura pu lui donner. 3 Chapitre 1 La reconnaissance faciale La reconnaissance faciale s’inscrit dans le domaine plus vaste de la vision par ordinateur, qui part du constat que le sens le plus utilisé par l’homme est la vue. Dès lors, il peut s’avérer très utile de donner des « yeux » à son ordinateur : ainsi, il peut devenir capable de remplacer ceux de l’homme pour des tâches répétitives telles que la reconnaissance de nombreux visages, ou demandant des calculs conséquents comme le lissage d’une image ou l’augmentation de sa netteté. Le principe de la reconnaissance faciale est très simple : comme son nom l’indique, il s’agit d’identifier une face donnée. Selon les algorithmes et les buts recherchés, cette identification peut prendre plusieurs aspects : il peut s’agir de déterminer à qui appartient un visage, de décider si oui ou non ce visage est reconnu, ou même dans certains cas de déterminer s’il s’agit bien d’un visage. 1.1 Les faces propres : la théorie Nous cherchons à effectuer de la reconnaissance faciale, en employant des systèmes simples comme une webcam. Le sujet se plaçant alors face à la caméra, nous partons donc de la remarque, qui nous simplifie grandement notre démarche, selon laquelle nous pratiquons de la reconnaissance à deux dimensions. La méthode de reconnaissance faciale Eigenfaces emploie la technique de l’analyse en composante principale, qui marque une différence notable avec les méthodes plus classiques, appelées méthodes géométriques ou locales, qui se basent sur les particularités du visage analysé, et dont les défauts résident dans son manque de précision, ainsi que sa sensibilité aux informations qui ne sont pas pertinentes. La méthode que nous avons utilisé est qualifiée de globale, puisque l’ensemble du visage est alors analysé. Notre technique de reconnaissance va donc utiliser la méthode d’analyse en composantes principales (également dite ACP).De manière simple, nous visons la diminution de la dimension de l’espace dans lequel nous allons travailler, et nous pourrons alors simplifier les données à notre disposition et leur interprétation. 4 Ainsi nous pourrons prendre en compte les informations importantes qui nous permettrons de reconnaître un visage parmi d’autres avec un bon taux de réussite.(Figure 1.1) Figure 1.1 – Illustration de l’ACP Faisons un simple parallèle entre ma méthode des faces propres et les séries de Fourier. Les séries de Fourier nous permettent généralement de représenter un signal périodique à l’aide de sommes de cosinus et de sinus.(Figure 1.2) Figure 1.2 – Approximation d’un créneau par des sinusoïdales Dans le cas de notre projet, notre approche consiste à représenter un visage comme étant la combinaison linéaire d’un ensemble d’images, ces dernières formant une base de référence. Mathématiquement, cela revient à parvenir à l’équation : Φi = n X pi di i=1 où di représente le visage propre, et pi le coefficient associé. Nous allons chercher à trouver les visages propres ; tout d’abord, nous devons prendre un nombre M de visages d’apprentissage. Chacune de ces images, qui sont en pratique des matrices N × N sont alors transformées en un unique vecteur colonne de longueur N 2 . Matrice N × N initiale : α11 α21 ... αN 1 α12 α22 ... αN 2 α1i α2i ... αN i 5 α1N α2N ... αN N transformée en : α11 .. . αN 1 .. . α1N . .. αN N Nous devons par la suite déterminer le visage moyen, déduit des M visages d’apprentissages. Ψ= M 1 X Γi M i=1 Ce visage moyen va servir dans l’analyse d’images, on soustrait en effet ce visage moyen aux visages d’apprentissages, ce qui nous laisse alors les informations propres à ce visage, nous récupérons alors dans Φi uniquement les informations qui sont particulières à ce visage d’apprentissage. Φi = Γi − Ψ Où Φi représente le ieme visage auquel on a soustrait le visage moyen. A présent nous devons calculer la matrice de covariance D. Elle correspond à D = QQT , avec Q = [Φ1 , Φ2 , ....., ΦM ] Nous devrions calculer les vecteurs propres di de la matrice D. Mais cela représente pour nous N 2 vecteurs propres de dimension N 2 chacun. C’est à présent que nous allons réduire l’information en limitant les composantes avec lesquelles nous travaillerons, en accord avec le principe de l’analyse en composantes principales. Nous allons donc considérer la matrice E = QT Q, dont nous trouverons les vecteurs propres ei . Cette matrice est de taille M × M ce qui nous simplifiera donc les choses étant donné que nous aurons M vecteurs propres de taille M chacun. Le passage de la matrice D à la matrice E n’est pas anodin, nous utilisons en effet le fait que les vecteurs propres de ces deux matrices sont liés de manière assez proche. En effet, nous avons comme relation, Eei = QT Qei = λi ei avec λi la valeur propre associée au vecteur propre ei . 6 En multipliant cette équation par la matrice Q, il vient QEei = QQT Qei Nous voyons alors apparaître la matrice D QEei = DQei = λi Qei Nous en déduisons donc qu’avec ei vecteur propre de la matrice E associé à la valeur propre λi , nous avons par conséquent Qei est un vecteur propre de la matrice D associé à la même valeur propre λi . Ainsi, nous avons di vecteur propre de D, avec di = Qei Ce sont les valeurs propres qui leur sont associées qui nous permet ensuite de classer les vecteurs propres en fonction de leur capacité à caractériser les variations entre les images. Lorsque l’on les visualise (ces vecteurs sont à l’origine des matrices de taille N × N ), les faces propres sont ce que l’on pourrait appeler des images aux airs fantomatiques. Mais gardons à l’esprit que sont les vecteurs propres de la matrice de covariance des images d’apprentissage des visages. (Figure 1.3) Figure 1.3 – Quelques faces propres Les M vecteurs propres que nous avons alors obtenus nous permettrons donc d’approximer au mieux les visages d’apprentissage en utilisant les visages propres de plus grande importance. L’avantage de réduire le nombre de visages propres est d’une part de nécessiter de moins d’espace mémoire, mais aussi de réduire les calculs, leur temps d’exécution ; cependant nous perdons sans aucun doute de l’information et donc l’information moins précise, mais les résultats ne s’en verront pas vraiment modifiés, étant donné que nous ne nous donnons qu’une mission d’identification. Nous ne cherchons pas à reconstruire le visage du sujet à partir de nos visages propres, mais seulement à le reconnaître. Parmi les M vecteurs propres trouvés, nous allons seulement conserver un nombre L, qui seront les plus significatifs. Nous allons trouver maintenant le poids associé à chacun des visages propres. Les images servant à l’apprentissage, auquel on a enlevé l’image moyenne, sont en fait combinaison linéaire des visages propres. 7 Φi = L X pi di i=1 Pour trouver le poids associé, nous faisons pour chacune des coordonnées correspondant à un visage d’apprentissage pi = dTi Φi Ce qui nous permet d’obtenir pour chacun des M visages d’apprentissages un vecteur Πi , où i représente le ieme visage, et qui nous informe sur le coefficientage appliqué à chacun des visages propres. Πi = p1 p2 .. . pL Passons à présent au travail à effectuer pour la reconnaissance d’un visage d’un sujet. Une fois l’image prise, l’image (vecteur colonne Γ) obtenue est soustraite à l’image moyenne Ψ : Φ=Γ−Ψ Puis nous trouvons les coordonnées de cette image dans l’espace réduit des faces propres pi = dTi Φi Ce qui nous donne au final : Π= p1 p2 .. . pL Il nous faut maintenant interpréter la projection de l’image à analyser pour identifier le sujet. Pour cela nous allons utiliser une mesure de distance particulière, la distance de Mahalanobis. L’intérêt de cette distance réside dans le fait qu’elle va accorder un poids moins important aux composantes bruitées, et qu’elle permet séparer efficacement les axes pour lesquels l’information peut être mieux classifiée. (Figure 1.4) 8 Figure 1.4 – Illustration de la distance de Mahalanobis Elle est définie par : d(a, b) = q (a − b)T Q−1 (a − b) avec Q covariance des variables. Nous cherchons donc : m = min kΠ − Πi k Puis, nous comparons la valeur de m trouvée à une valeur seuil ∆, qui aurait du être déterminée à partir d’essais sur des images choisies aléatoirement, qui peuvent aussi bien représenter des visages qu’autres choses, puis en comparant ces valeurs aux valeurs obtenues avec des visages d’apprentissage, et décider du seuil que nous avons choisi. Mais le choix de ce seuil dépend de trop nombreuses conditions ( prise de vue des images, niveau de précision souhaité pour la reconnaissance, etc..) c’est pour cela qu’en pratique nous avons décidé de ne pas prendre de seuil. L’utilisation d’un seuil nous aurait permis de déterminer si oui ou non l’image analysée correspond à un visage présent dans la base de données. Si alors la valeur de m était inférieure à celle du seuil, l’image correspond au visage qui a donné cette valeur la plus basse. Dans notre cas, le fait de ne pas choisir de seuil à eu pour conséquence que lorsqu’un visage non présent dans la base de donnée était testé, il était tout de même reconnu par le programme. 1.2 Les faces propres : l’implantation pratique Voici donc le code de l’implantation en Python, commenté : 1 2 3 4 5 6 # !/ usr / bin / env python2 # -* - coding : utf -8 -* import cv import math import numbers 9 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import import import import numpy as np os sys time def list_de_iplimage ( image ): " Renvoie ␣ la ␣ forme ␣ liste ␣ d ’ une ␣ image ␣ OpenCV " return [[ cv . Get2D ( image , i , j )[0] for j in xrange ( image . width )] for i in xrange ( image . height )] class Tableau : def __add__ ( self , autre ): " Additionne ␣ avec ␣ un ␣ Tableau ␣ ou ␣ un ␣ scalaire " if isinstance ( autre , Tableau ): return Tableau ( self . tab + autre . tab ) elif isinstance ( autre , numbers . Number ): return Tableau ( self . tab + autre ) else : raise TypeError ( " Tableau . __add__ ␣ : ␣ Tableau ␣ ou ␣ scalaire ␣ attendu " def __div__ ( self , autre ): " Divise ␣ par ␣ un ␣ scalaire " return Tableau ( self . tab / autre ) def __getitem__ ( self , index ): " Renvoie ␣ un ␣ coefficient ␣ du ␣ Tableau , ␣ pour ␣ pouvoir ␣ utiliser ␣ tableau [ i if isinstance ( index , int ) and self . largeur != 1: return Tableau ( self . tab [ index ]) else : return self . tab [ index ] def __init__ ( self , source , nom = str ( time . time ())): """ Constructeur d ’ un objet Tableau On peut lui passer : * une c h a n e de c a r a c t r e , qui sera le chemin de l ’ image charg * une image au format OpenCV passer en Tableau * un array numpy passer en Tableau """ if isinstance ( source , str ): self . tab = np . array ( list_de_iplimage ( cv . LoadImage ( source , cv . C V elif isinstance ( source , cv . iplimage ): self . tab = nb . array ( list_de_iplimage ( source )) elif isinstance ( source , ( np . ndarray , list )): self . tab = np . array ( source ) else : raise TypeError ( " Tableau . __init__ ␣ : ␣ % s ␣ inattendu " % type ( source self . nom = nom self . calc_dimensions () 10 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 def __len__ ( self ): " Pour ␣ pouvoir ␣ utiliser ␣ len ( tableau ) ␣ au ␣ cas ␣ o return len ( self . tab ) " def __mul__ ( self , autre , el e me n t _p a r_ e le m e nt = False ): """ Multiplie : multiplication de matrice , ou par un scalaire Le flag e l em e nt _ p ar _ el e me n t indique , s ’ il est vrai , qu ’ on n ’ utilise la multiplication de l ’ a l g b r e des matrices , mais une multiplicati lment par lment de deux matrices de m m e s dimensions """ if isinstance ( autre , Tableau ): if el e me n t_ p a r_ e le m e nt : return Tableau ( self . tab * autre . tab ) else : resultat = np . dot ( self . tab , autre . tab ) if isinstance ( resultat , numbers . Number ): return resultat else : return Tableau ( resultat ) elif isinstance ( autre , numbers . Number ): return Tableau ( self * autre ) else : raise TypeError ( " Tableau . __mul__ ␣ : ␣ Tableau ␣ ou ␣ scalaire ␣ attendu " def __setitem__ ( self , index , valeur ): """ Modifie un coefficient du Tableau : i m p l m e n t e tableau [ index ] = valeur """ self . tab [ index ] = valeur def __sub__ ( self , autre ): " Additionne ␣ avec ␣ un ␣ Tableau ␣ ou ␣ un ␣ scalaire " if isinstance ( autre , Tableau ): return Tableau ( self . tab - autre . tab ) elif isinstance ( autre , numbers . Number ): return Tableau ( self . tab - autre ) else : raise TypeError ( " Tableau . __sub__ ␣ : ␣ Tableau ␣ ou ␣ scalaire ␣ attendu " def afficher ( self , nom = None ): """ Affiche le Tableau , comme une image , dans une f e n t r e Utilise OpenCV pour cela """ if nom is None : nom = self . nom cv . ShowImage ( nom , self . vers_iplimage ()) def calc_dimensions ( self ): " Calcule ␣ les ␣ dimensions ␣ du ␣ Tableau " 11 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 self . hauteur = len ( self . tab ) try : self . largeur = len ( self . tab [0]) except TypeError : self . largeur = 1 def c a l c _ e l e m e n t s _ p r o p r e s ( self ): " Calcule ␣ valeurs ␣ propres ␣ et ␣ vecteurs ␣ propres ␣ du ␣ tableau " valeurs , vecteurs = np . linalg . eig ( self . tab ) self . val_propres = list ( valeurs ) self . vect_propres = [ Tableau ( vect ) for vect in vecteurs ] def covariance ( self ): " Renvoie ␣ le ␣ Tableau ␣ de ␣ covariance ␣ du ␣ Tableau " return self * self . transposee () def dimensions ( self ): " Renvoie ␣ les ␣ dimensions ␣ -␣ hauteur ␣ * ␣ largeur ␣ -␣ du ␣ tableau " return self . hauteur , self . largeur def en_colonne ( self ): " Renvoie ␣ le ␣ Tableau ␣ comme ␣ un ␣ vecteur ␣ colonne " return self . redimensionnee ( -1) def inverse ( self ): " Renvoie ␣ l ’ inverse ␣ du ␣ Tableau ␣ dans ␣ l ’ a g b r e ␣ des ␣ matrices " return Tableau ( np . linalg . inv ( self . tab )) def mahalanobis ( self , autre , cov ): " Calcule ␣ la ␣ distance ␣ de ␣ Mahalanobis ␣ entre ␣ deux ␣ Tableaux " a = ( self - autre ) * cov . inverse () * ( self - autre ) return math . sqrt ( a ) def met tre_en_c olonne ( self ): " Met ␣ le ␣ Tableau ␣ en ␣ forme ␣ de ␣ vecteur ␣ colonne " self . redimensionner ( -1) def redimensionnee ( self , nouvelle_forme ): """ Renvoie le Tableau sous une nouvelle forme Le nombre total d ’ lments doit rester le m m e """ tab = Tableau ( np . reshape ( self . tab , nouvelle_forme )) tab . calc_dimensions () return tab def redimensionner ( self , nouvelle_forme ): """ Change la forme du Tableau Le nombre total d ’ lments reste le m m e """ self . tab = np . reshape ( self . tab , nouvelle_forme ) self . calc_dimensions () 12 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 def transposee ( self ): " Renvoie ␣ la ␣ t r a n s p o s e ␣ du ␣ Tableau " return Tableau ( np . transpose ( self . tab )) def transposer ( self ): " Transpose ␣ le ␣ Tableau " self . tab = np . transpose ( self . tab ) def vers_iplimage ( self , profondeur = cv . IPL_DEPTH_8U ): " Renvoie ␣ l ’ iplimage ␣ quivalente ␣ ( format ␣ OpenCV ) " temp = cv . CreateImage (( self . largeur , self . hauteur ) , cv . IPL_DEPTH_64F , 1) for (i , ligne ) in enumerate ( self . tab ): for (j , valeur ) in enumerate ( ligne ): cv . Set2D ( temp , i , j , valeur ) img = cv . CreateImage (( self . largeur , self . hauteur ) , profondeur , 1) cv . Convert ( temp , img ) return img class Collection : def __init__ ( self , chemin = " faces / " ): " Constructeur ␣ d ’ un ␣ objet ␣ Collection " self . liste_faces = [ Tableau ( chemin + fichier ) for fichier in os . listdir ( chemin )] self . nb_faces = len ( self . liste_faces ) # On p r p a r e la Collection pour la reconnaissance " self . calc_moyenne () self . calc_ecarts () self . calc_covariance () self . c alc _f ac es_ pr op re s () self . calc_coeff_faces () def calc_covariance ( self ): " Calcule ␣ la ␣ covariance ␣ des ␣ faces ␣ n o r m a l i s e s ␣ de ␣ la ␣ Collection " self . covariance = self . ecarts . transposee (). covariance () def calc_moyenne ( self ): " Calcule ␣ la ␣ moyenne ␣ des ␣ faces ␣ de ␣ la ␣ Collection " self . moyenne = reduce ( Tableau . __add__ , self . liste_faces ) / self . nb_ def calc_coeff_faces ( self ): """ Calcule les coefficients des faces de la collection , dans la base des faces propres """ self . coeff_faces = Tableau ([ self . coeff ( face ) for face in self . ecarts . transposee ()]) 13 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 self . covar_coeff = self . coeff_faces . transposee (). covariance () def calc_ecarts ( self ): " Calcule ␣ le ␣ Tableau ␣ des ␣ carts ␣ ␣ la ␣ moyenne " self . ecarts = Tableau ([( face - self . moyenne ). en_colonne (). tab for face in self . liste_faces ]). transposee () def c al c_ fac es _p rop re s ( self ): " Calcule ␣ les ␣ faces ␣ propres ␣ de ␣ la ␣ collection " self . covariance . c a l c _ e l e m e n t s _ p r o p r e s () self . faces_propres = [ self . ecarts * vect for vect in self . covariance . vect_propres ] # Pour un affichage , redimensionner en (100 , 80) def coeff ( self , tableau ): " Renvoie ␣ les ␣ coefficients ␣ d ’ un ␣ tableau ␣ sur ␣ la ␣ base ␣ des ␣ faces ␣ propre return [ face * tableau . en_colonne () for face in self . faces_propres ] def reconnaitre ( self , image ): """ Reconnait ou non une image . Sauvegarde la face connue la plus proche dans le fichier resultat . b et renvoie la distance entre le visage r e c o n n a t r e et cette fac coeff = Tableau ( self . coeff ( image - self . moyenne )) distances = [ coeff . mahalanobis ( coeff_face , self . covar_coeff ) for coeff_face in self . coeff_faces ] distance_min = min ( distances ) i = distances . index ( distance_min ) cv . SaveImage ( " resultat . bmp " , self . liste_faces [ i ]. vers_iplimage ()) return distance_min if __name__ == " __main__ " : collection = Collection () # On passe en argument le visage reconna tre visage = Tableau ( sys . argv [1]) # On utilise la m t h o d e reconnaissance et on affiche le r s u l t a t en s # L ’ interface s ’ occupe du reste print collection . reconnaitre ( visage ) 14 Chapitre 2 L’interface graphique Une fois le moteur réalisé, il nous faut trouver un moyen pratique pour l’utilisateur afin de s’en servir. Si la ligne de commande peut s’avérer très utile et pratique, elle est a priori moins accessible pour quelqu’un qui ne connait pas bien le fonctionnement du programme. Nous avons donc cherché à réaliser une interface graphique. En plus de fournir un moyen commode d’utiliser le moteur codé précédemment, elle sera utilisable avec n’importe quel moteur de reconnaissance faciale utilisant un format commun. Ainsi, elle permettra de tester un algorithme de reconnaissance de façon ergonomique en quelques clics, sans que le programme l’implémentant n’ait besoin d’effectuer les manipulations communes de lecture à la webcam et d’affichage du résultat. Elle permettra également de choisir quel moteur utiliser, et il sera possible d’en ajouter de nouveaux à l’aide d’un fichier de configuration simple (qui aurait du être également généré par une interface graphique, ce que nous n’avons pas pu faire en raison d’un manque de temps). 2.1 Fonctionnement général Afin de pouvoir utiliser le module de reconnaissance voulu, le logiciel doit respecter une certaine architecture. L’interface graphique doit être correctement séparée du moteur, et ils doivent respecter un certain format de communication. Ainsi, on pourra utiliser n’importe quel moteur respectant ce format avec notre interface. De plus, notre logiciel doit pouvoir proposer plusieurs modules, et on doit pouvoir en rajouter dans le temps. Ils ne seront donc pas implantés en dur mais il les chargera dynamiquement à partir d’une liste facilement modifiable et extensible. Le format choisi pour l’appel des modules est relativement simple tout en restant flexible. Ils doivent être appelables à l’aide d’un appel système, qui doit accepter parmi ses arguments le chemin d’une image qui contient le visage à reconnaître. Cet argument peut être placé n’importe où dans la ligne de commande à lancer. Il devra lire cette image, et écrire une autre image, qui devra contenir le visage de la personne a priori reconnue. Elle pourra également afficher un 15 texte sur la sortie standard, qui contiendra des informations supplémentaires, comme par exemple le degré de confiance, le nom de la personne... Ces modules seront listés dans un fichier au format JSON, qui contiendra leur nom, la commande à lancer pour les exécuter (formatée avec le nom de l’image à lire) et une description du module. L’interface graphique commencera par afficher une image correspondant à la capture de la webcam. Une touche permettra de prendre la photo. Ensuite, une fenêtre sera affichée avec divers composants. Une liste déroulante permettra de sélectionner le module souhaité, et un bouton permettra de rafraîchir cette liste si on a modifié le fichier JSON depuis le début de l’affichage. La description du module sera également affichée en dessous. Enfin, il y aura un bouton permettant d’appeler le module choisi pour effectuer la reconnaissance. Il était normalement prévu d’avoir la capture webcam affichée en même temps que la fenêtre principale, mais nous n’avons pas réussi à comprendre comment afficher cette capture dans PySide. Nous deviosn également fournir une interface pour ajouter simplement des modules sans éditer à la main le fichier json, mais nous n’avons pas eu le temps de réaliser ces fonctions. 2.2 Implantation python Cette interface graphique utilise la librairie PySide, un portage de Qt pour le langage Python. En PySide, chaque élément graphique est un objet dont la classe hérite de QWidget (un WInDow gadGET). Il existe de nombreux widgets prédéfinis. Pour personnaliser son widget, on crée une classe héritant de la classe correspondante. Par exemple, nous voulons que le pointeur de la souris devienne la « main » lorsqu’il passe au dessus d’un bouton cliquable. Nous allons donc redéfinir notre propre classe de boutons cliquables : 1 2 3 4 5 6 class Bouton ( QPushButton ): """ Classe utilisee pour changer le pointeur quand on passe la souris sur un bouton """ def __init__ ( self , texte , parent = None ): super ( Bouton , self ). __init__ ( texte , parent ) self . setCursor ( Qt . Poi nt in gHa nd Cu rso r ) Ici, on ne change qu’un comportement défini une seule fois à l’initialisation du bouton. On ne doit donc réécrire que le constructeur. On appelle le constructeur parent, et on rajoute dans ce constructeur le paramétrage souhaité du pointeur. Ainsi, chaque bouton que l’on créera en utilisant cette classe Bouton profitera de ce réglage. La classe Fenêtre, elle, dispose de plus de réglages. On y spécifiera en particulier les différents widgets qu’elle contient, disposés à l’aide d’un QGridLayout, qui permet de donner leurs positions (dans des coordonnées entières) dans une grille qui couvre la fenêtre. Afin de gérer les évènements, on utilise le mécanisme de signaux et de slots de Qt. Un évènement (par exemple, le clic sur un bouton) correspond à un « signal », que l’on connecte ) un « slot », c’est à dire à une fonction qui sera exécutée lors de son déclenchement. Les signaux sont ici des méthodes de la classe Fenetre. 16 Le code final de l’interface graphique est donc le suivant, suivi par une capture d’écran : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 # !/ usr / bin / env python2 # -* - coding : utf -8 -* import cv import json import os import sys from PySide . QtCore import * from PySide . QtGui import * class Bouton ( QPushButton ): """ Classe u t i l i s e pour changer le pointeur quand on passe la souris sur un bouton """ def __init__ ( self , texte , parent = None ): super ( Bouton , self ). __init__ ( texte , parent ) self . setCursor ( Qt . Po int in gH and Cu rs or ) class Fenetre ( QWidget ): def __init__ ( self , parent = None ): super ( Fenetre , self ). __init__ ( parent ) # C r a t i o n des lments self . setWindowTitle ( " Thira " ) self . liste_modules = QComboBox () # liste d r o u l a n t e self . recha rger_mod ules = Bouton ( " Recharger ␣ les ␣ modules " ) self . description = QLabel () self . action = Bouton ( " Action ␣ ! " ) # Placement des lments layout = QGridLayout () layout . addWidget ( self . liste_modules , 0 , 0) layout . addWidget ( self . recharger_modules , 0 , 1) layout . addWidget ( self . description , 1 , 0 , 1 , 2) layout . addWidget ( self . action , 2 , 0 , 1 , 2) self . setLayout ( layout ) # C r a t i o n des slots self . liste_modules . c ur r en t I nd e xC h a ng e d . connect ( self . changer_module ) self . recha rger_mod ules . clicked . connect ( self . charger_modules ) self . action . clicked . connect ( self . reconnaitre ) # Autoriser le passage la ligne dans la description # Sinon elle est en une ligne et on ne peut pas r d u i r e la taille self . description . setWordWrap ( True ) # On charge les modules disponibles self . charger_modules () 17 la c r a t i o n de la f e n t r e 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 def reconnaitre ( self ): " U t i l i s e ␣ lorsqu ’ on ␣ demande ␣ ␣ r e c o n n a t r e ␣ le ␣ visage ␣ pris ␣ en ␣ phot commande = self . modules [ self . liste_modules . currentIndex ()][ " command # La commande est sous la forme : # n o m _ d u _ p r o g r a m m e _ a _ e x e c u t e r {0} # on a " c {0} d ". format (" bla ") == " cblad " # Le moteur r p o n d sur la sortie standard # Actuellement on se contente de le laisser l ’ afficher , # on pourrait le rediriger vers une variable pour l ’ exploiter # par exemple en l ’ affichant dans une f e n t r e os . system ( commande . format ( " visage . bmp " )) def changer_module ( self , texte ): """ Lorsqu ’ on change de module l ’ aide de la liste d r o u l a n t e , on actualise l ’ affichage de la description """ self . description . setText ( self . modules [ self . liste_modules . currentIndex ()][ " description " ]) def charger_modules ( self ): """ Pour recharger la liste des modules si on a m o d i f i le fichier modules . js qui contient leur description sous format json """ self . liste_modules . clear () self . modules = json . loads ( open ( " modules . js " ). read ()) for module in self . modules : self . liste_modules . addItem ( module [ " nom " ]) if __name__ == " __main__ " : # Ce code ne s ’ e x c u t e que si on e x c u t e directement le module ( pas e webcam = cv . CaptureFromCAM (0) print " Appuyez ␣ sur ␣ S ␣ pour ␣ prendre ␣ la ␣ photo " while True : image = cv . GetSubRect ( cv . QueryFrame ( webcam ) , (281 , 191 , 80 , 100)) cv . ShowImage ( " webcam " , image ) key = cv . WaitKey (1) # La touche pour prendre la photo est la touche s # Sous le linux u t i l i s pour tester , la valeur correspondante # tait 1048691. # Sous Windows , la valeur tait ord (" s ") , soit 115. # Sous certaines machine , cette valeur peut tre " s ". if key in [1048691 , ord ( " s " ) , " s " ]: cv . SaveImage ( " visage . bmp " , image ) break # thira = THIbault RAlph # sys . argv = les arguments de la commande e x c u t e , # par exemple [" interface . py " , " - o3 " , " visage . bmp "] # Inutile pour nous mais d e m a n d dans le constructeur de QApplication thira = QApplication ( sys . argv ) 18 98 99 100 fenetre = Fenetre () fenetre . show () sys . exit ( thira . exec_ ()) Figure 2.1 – Capture d’écran du logiciel (Arch Linux, gestionnaire de fenêtre Awesome) 19 Conclusion Ce projet nous a permis de découvrir plus profondément plusieurs aspects du développement d’un logiciel complexe. Il nous a fallu d’abord nous renseigner sur le côté algorithmique de la reconnaissance de visage, et plus généralement de la vision par ordinateur, qui est un domaine en pleine expansion. Cette recherche nous a donc mené à la réalisation d’un moteur de reconnaissance faciale « brut ». Il nous a fallu résoudre plusieurs problèmes algorithmiques ayant plus ou moins de rapport avec les mathématiques, discipline importante dans le traitement d’images en général. Ensuite, nous avons du concevoir une interface graphique. Il nous a fallu pour cela comprendre les mécanismes de fonctionnement de ces outils particuliers, qui ne suivent pas une structure impérative linéaire mais plutôt un modèle déclaratif, où la bibliothèque utilisée se charge de tout gérer une fois qu’on a décrit le comportement attendu. Dans la continuité du cours de Génie Logiciel et dans une démarche d’ingénieur, nous avons également du organiser notre logiciel final de façon à le rendre modulaire et extensible par la suite. Ainsi, la réalisation de ce projet nous a permis de comprendre plus en profondeur le développement d’un logiciel, de la conception à la réalisation en passant par la réflexion logique et algorithmique. Si nous avions plus de temps, nous aurions aimé développer un peu plus l’interface graphique afin de compléter son rôle (il ne se serait toutefois agit principalement que de réutiliser des concepts déjà présents dans notre code final). Il aurait été également intéressant d’affiner le moteur de reconnaissance, en trouvant un moyen de prendre des images correctement cadrées et en déterminant les seuils de façon rigoureuse. Si d’autres projets se font sur ce thème, il serait sans doute intéressant de leur fournir l’interface graphique afin de tester une utilisation par des utilisateurs externes. 20