PASCAL
SANDREZ

Détection de visage

Je cherchais à faire une vidéo avec plein de photos (une par jour) en forme de time lapse. Pour cela il faut que toutes les photos soient redimensionnées, cadrés et orientés pour le que le visage soit toujours au même endroit et de la même taille. J'ai déjà traité plein d'images à la main il y a quelque temps, c'est franchement long et approximatif. Il me fallait donc une meilleure solution. Il y a pas mal de logiciels disponibles sur internet pour faire ça mais après en avoir testé plusieurs, je n'ai finalement pas trouvé quelque chose qui me convienne sous Windows. Mais je suis souvent tombé sur des logiciels qui utilisent OpenCV (Open Computer Vision). J'ai donc testé OpenCV avec Python (que je trouve plus pratique que C ou C++) sous Windows.

Installation de Python et OpenCV

Je suis tombé dans quelques pièges à l'installation. Quelle version de python ? Quelle version d'opencv ? 32bits ? 64bits ?

Pour installer Python j'ai pris la version 2.7.10 (et non une version 3.x) car c'est la version qu'il faut pour OpenCV 2.4 (il faut une 2.7.x) et que c'est la version la plus répandue et la plus pratique à cette heure ci. Je choisis la version 32 bits. Je laisse les options par défaut, il s'installe dans C:\Python27

Ensuite j'installe Numpy 1.9.2 version 32 bits aussi. Il détecte automatiquement l'emplacement de python et s'installe sans problème.

Pour installer openCV j'ai téléchargé sur le site d'OpenCV la version 2.4.10. Bien que la version 3.0 est disponible, j'ai préféré la version 2.4 sur laquelle je peux trouver beaucoup d'exemples et de documentation. L'installateur extrait en fait les librairies. Je copie le fichier <opencvfolder>\build\python\x86\2.7\cv2.pyd et je le colle dans le dossier d'install de Python, dans C:\Python27\Lib\site-packages\

Si tout va bien il suffit de lancer python (avec Python IDLE dans le menu démarrer ou bien taper python dans une console) et d'importer OpenCV.

import cv2

Si il y a aucun message en tout cas pas d'erreur c'est que c'est bon.

Détection du visage

Images à traiter
Images à traiter
Pour l'exemple je vais traiter plusieurs images ou l'on peut voir que le visage n'est pas toujours a la même position, le zoom varie ainsi que l'inclinaison du visage sur la photo. Il faut compenser tous ces effets.

Premièrement on importe OpenCV et quelques autres modules nécessaires.

import cv2
import numpy
import math

Ensuite il faut créer les classifiers. Ils sont créés à partir d'un fichier xml d'entrée qui contient les caractéristiques des détails à détecter. Quelques fichiers xml sont fournis avec l'installation d'OpenCV. Ils se trouvent dans <opencvfolder>\sources\data\haarcascades\. Il est aussi possible d'entrainer le classifier pour détecter n'importe quoi. A partir d'un jeu de photos à reconnaitre et d'un jeu de photos qui ne contiennent pas l'objet à reconnaitre, l'algorithme génère un fichier xml qui contient les caractéristiques de l'image à reconnaitre. Mais ici on utilise les fichiers xml déjà existants.

# Create the haar cascade
leCascade = cv2.CascadeClassifier('haarcascade_mcs_lefteye.xml')
reCascade = cv2.CascadeClassifier('haarcascade_mcs_righteye.xml')
mouthCascade = cv2.CascadeClassifier('haarcascade_mcs_mouth.xml')

Il y a bien un un classifier pour détecter le visage complet mais il donne la position globale du visage, il pourra donner qu'approximativement la taille du visage et pas l'orientation. En détectant chacun des deux yeux j'obtiens vraiment la position, la taille (le zoom) et l'orientation du visage. Par contre cela génère beaucoup de faux positifs, les deux yeux étant souvent similaires et des ombres peuvent ressembler à un oeil. Donc en plus des deux yeux je détecte la bouche. Ca permet de trier les éléments pour sélectionner les bons. Je mesure pour cela l'écartement entre la bouche et les yeux ainsi qu'entre les deux yeux. Comme les écartements sont à peu près identiques, j'utilise cette caractéristique pour sélectionner la bonne combinaison.

On lit l'image à traiter et on la convertie en niveau de gris, ce qui est plus adapté pour traiter les images avec OpenCV.

# Read the image
image = cv2.imread(imagePath)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

Puis on détecte les yeux et la bouche.

def findRightEye(image):
    # Detect left eyes in the image
    righteyes = reCascade.detectMultiScale(
        image,
        scaleFactor=1.2,
        minNeighbors=5,
        minSize=(80, 50),
        flags = cv2.cv.CV_HAAR_SCALE_IMAGE
    )
    return righteyes

def findLeftEye(image):
    # Detect left eyes in the image
    lefteyes = leCascade.detectMultiScale(
        image,
        scaleFactor=1.2,
        minNeighbors=5,
        minSize=(80, 50),
        flags = cv2.cv.CV_HAAR_SCALE_IMAGE
    )
    return lefteyes

def findMouth(image):
    # Detect mouthes in the image
    mouthes = mouthCascade.detectMultiScale(
        image,
        scaleFactor=1.2,
        minNeighbors=5,
        minSize=(80, 50),
        flags = cv2.cv.CV_HAAR_SCALE_IMAGE
    )
    return mouthes

Le paramètre scaleFactor détermine le facteur d'échelle qui est appliqué entre deux conversions. Il faut donc qu'il soit supérieur à 1. Plus il est grand plus ce sera rapide mais moins ce sera précis. Le paramètre minSize permet de détecter des éléments plus ou moins petits. Le paramètre minNeighbors défini combien d'objets doivent être détectés autour de l'objet initial pour valider la détection de cet objet. En réalité il faut jouer avec les paramètre selon l'image à traiter, il n'y a pas vraiment de valeur idéale

On peut afficher un rectangle autour des objets détectés.

righteyes = findRightEye(gray)
print 'found '+str(len(righteyes))+' right eye (green)'
print righteyes
for (x, y, w, h) in righteyes:
    cv2.rectangle(image, (x, y), (x+w, y+h), (0, 255, 0), 10) # Draw a rectangle around the eye

lefteyes = findLeftEye(gray)
print 'found '+str(len(lefteyes))+' left eyes (blue)'
print lefteyes
for (x, y, w, h) in lefteyes:
    cv2.rectangle(image, (x, y), (x+w, y+h), (255, 0, 0), 10) # Draw a rectangle around the eye

mouthes = findMouth(gray)
print 'found '+str(len(mouthes))+' mouth (red)'
print mouthes
for (x, y, w, h) in mouthes:
    cv2.rectangle(image, (x, y), (x+w, y+h), (0, 0, 255), 10) # Draw a rectangle around the mouth
Les éléments détectés sur les images
Les éléments détectés sur les images

En rouge est affiché le résultat de la détection de la bouche, en vert est affiché le résultat de la détection de l'oeil droit et en bleu le résultat de la détection de l'oeil gauche.

Puis on affiche le résultat.

cv2.imshow("Faces found" ,image)
cv2.waitKey(0)

On peut voir les multiples détection des yeux et de la bouche, il est donc indispensable d'éliminer les faux positifs.

Traitement de l'image

La deuxième étape c'est de sélectionner la combinaison des yeux et bouche parmi tous les éléments trouvés et d'utiliser les données pour traiter l'image. Pour chaque combinaison on calcule le centre de chacun des éléments détectés et la distance entre chaque.

for (xre, yre, wre, hre) in righteyes:
    for (xle, yle, wle, hle) in lefteyes:
        for (xmo, ymo, wmo, hmo) in mouthes:
            cxre = xre+wre/2.0 # x position of right eye center
            cyre = yre+hre/2.0 # y position of right eye center
            cxle = xle+wle/2.0 # x position of left eye center
            cyle = yle+hle/2.0 # y position of left eye center
            cxmo = xmo+wmo/2.0 # x position of mouth center
            cymo = ymo+hmo/2.0 # y position of mouth center
            dxrl = math.sqrt(pow((cxre)-(cxle),2)+pow((cyre)-(cyle),2)) # distance between right eye and left eye
            dxlm = math.sqrt(pow((cxle)-(cxmo),2)+pow((cyle)-(cymo),2)) # distance between left eye and mouth
            dxmr = math.sqrt(pow((cxmo)-(cxre),2)+pow((cymo)-(cyre),2)) # distance between mouth and right eye

Si l'espacement entre les yeux ainsi qu'entre les yeux et la bouche sont similaires, c'est que c'est la bonne combinaison.

if (abs(dxmr/dxlm - 1) < threshold) and (abs(dxlm/dxrl - 1) < threshold) and (abs(dxrl/dxmr - 1) < threshold) and cxre > cxle and cymo > cyle and cymo > cyre:

A partir de là on peut calculer l'angle que forment les deux yeux avec l'horizontale.

angle = math.degrees(numpy.arctan((cyre-cyle)/(cxre-cxle)))

La fonction getRotationMatrix2D permet de créer une matrice qui sera appliquée à l'image pour effectuer la transformation. On peut lui passer un angle et un facteur d'échelle à appliquer.

intereyesdistance = 200 # final number of pixel between eyes
rot_mat = cv2.getRotationMatrix2D(((cxre+cxle)/2,(cyre+cyle)/2),angle,intereyesdistance/dxrl)

Pour faire la translation de l'image et positionner les yeux au milieu de l'image en largeur et un quart de la hauteur on modifie la matrice générée.

outwidth = 800
outheight = 800
iepx = outwidth/2.0
iepy = outheight*1.0/4.0
rot_mat[0][2] += iepx-(cxre+cxle)/2
rot_mat[1][2] += iepy-(cyre+cyle)/2

Et il suffit d'appliquer la matrice à l'image.

cv2.warpAffine(image, rot_mat, (outwidth,outheight),flags=cv2.INTER_LINEAR)
Le résultat
Le résultat

Et voilà. Sur chaque image le visage a la même taille, les deux yeux sont positionnées exactement au même endroit et cela sans effort. Ces images peuvent ensuite être facilement utilisés pour faire un time lapse.

Code final

import cv2
import numpy
import math

imagePath = 'in.jpg'
processedPath = 'processed.jpg'
outputPath = 'out.jpg'

threshold = 0.3

outwidth = 800
outheight = 800
iepx = outwidth/2.0
iepy = outheight*1.0/4.0
intereyesdistance = 200 # final number of pixel between eyes

# Create the haar cascade
leCascade = cv2.CascadeClassifier('haarcascade_mcs_lefteye.xml')
reCascade = cv2.CascadeClassifier('haarcascade_mcs_righteye.xml')
mouthCascade = cv2.CascadeClassifier('haarcascade_mcs_mouth.xml')

def findRightEye(image):
    # Detect left eyes in the image
    righteyes = reCascade.detectMultiScale(
        image,
        scaleFactor=1.2,
        minNeighbors=5,
        minSize=(80, 50),
        flags = cv2.cv.CV_HAAR_SCALE_IMAGE
    )
    return righteyes

def findLeftEye(image):
    # Detect left eyes in the image
    lefteyes = leCascade.detectMultiScale(
        image,
        scaleFactor=1.2,
        minNeighbors=5,
        minSize=(80, 50),
        flags = cv2.cv.CV_HAAR_SCALE_IMAGE
    )
    return lefteyes

def findMouth(image):
    # Detect mouthes in the image
    mouthes = mouthCascade.detectMultiScale(
        image,
        scaleFactor=1.2,
        minNeighbors=5,
        minSize=(80, 50),
        flags = cv2.cv.CV_HAAR_SCALE_IMAGE
    )
    return mouthes

# Read the image
image = cv2.imread(imagePath)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

outsize = max(image.shape[:2])

imagedebug = image.copy()

righteyes = findRightEye(gray)
print 'found '+str(len(righteyes))+' right eye (green)'
print righteyes
for (x, y, w, h) in righteyes:
    cv2.rectangle(imagedebug, (x, y), (x+w, y+h), (0, 255, 0), 10) # Draw a rectangle around the eye

lefteyes = findLeftEye(gray)
print 'found '+str(len(lefteyes))+' left eyes (blue)'
print lefteyes
for (x, y, w, h) in lefteyes:
    cv2.rectangle(imagedebug, (x, y), (x+w, y+h), (255, 0, 0), 10) # Draw a rectangle around the eye

mouthes = findMouth(gray)
print 'found '+str(len(mouthes))+' mouth (red)'
print mouthes
for (x, y, w, h) in mouthes:
    cv2.rectangle(imagedebug, (x, y), (x+w, y+h), (0, 0, 255), 10) # Draw a rectangle around the mouth

cv2.imshow("Faces found" ,imagedebug)
cv2.imwrite(processedPath, imagedebug)
cv2.waitKey(0)

for (xre, yre, wre, hre) in righteyes:
    for (xle, yle, wle, hle) in lefteyes:
        for (xmo, ymo, wmo, hmo) in mouthes:
            cxre = xre+wre/2.0 # x position of right eye center
            cyre = yre+hre/2.0 # y position of right eye center
            cxle = xle+wle/2.0 # x position of left eye center
            cyle = yle+hle/2.0 # y position of left eye center
            cxmo = xmo+wmo/2.0 # x position of mouth center
            cymo = ymo+hmo/2.0 # y position of mouth center
            dxrl = math.sqrt(pow((cxre)-(cxle),2)+pow((cyre)-(cyle),2)) # distance between right eye and left eye
            dxlm = math.sqrt(pow((cxle)-(cxmo),2)+pow((cyle)-(cymo),2)) # distance between left eye and mouth
            dxmr = math.sqrt(pow((cxmo)-(cxre),2)+pow((cymo)-(cyre),2)) # distance between mouth and right eye
            print str(dxrl)+' '+str(dxlm)+' '+str(dxmr)
            if (abs(dxmr/dxlm - 1) < threshold) and (abs(dxlm/dxrl - 1) < threshold) and (abs(dxrl/dxmr - 1) < threshold) and cxre > cxle and cymo > cyle and cymo > cyre:
                angle = math.degrees(numpy.arctan((cyre-cyle)/(cxre-cxle)))
                #print angle
                rot_mat = cv2.getRotationMatrix2D(((cxre+cxle)/2,(cyre+cyle)/2),angle,intereyesdistance/dxrl)
                #print rot_mat
                rot_mat[0][2] += iepx-(cxre+cxle)/2
                rot_mat[1][2] += iepy-(cyre+cyle)/2
                image = cv2.warpAffine(image, rot_mat, (outwidth,outheight),flags=cv2.INTER_LINEAR)
                cv2.imshow("Image processed" ,image)
                cv2.imwrite(outputPath, image)
                cv2.waitKey(0)