PASCAL
SANDREZ

Détection de visage avec OpenCV pour faire un time lapse

Un time lapse (vidéo accéléré) est une vidéo qui est largement accélérée ce qui permet de mettre en valeurs des changements assez lent sur le sujet filmé. C'est généralement fait en réduisant la cadence de prise de vue, une fois par seconde, une fois par heure, par jour, par mois, etc. Mais dans mon cas il s'agissait de traiter des centaines de photos, une par jour, prise chacune avec des conditions différentes et avec un modèle très mobile. J'ai donc écrit un petit script python qui les traite de façon semi-automatisé en utilisant OpenCV.

Depuis la naissance de notre première fille nous avons commencé à prendre une photo chaque jour pour la partager avec notre famille. Au bout d'un an j'ai eu l'idée d'en faire une vidéo, un time lapse. Mais les clichés ayant été fait de façon assez différentes chaque jour, il me fallait les modifier pour avoir le visage toujours à la même position et la même taille sur chaque image et pouvoir faire une vidéo qui montre une progression. La première année j'ai redimensionné chaque image à la main, changeant la taille, l'angle et la position du visage pour qu'il apparaisse à peu près au même endroit sur chaque image. Et bien évidemment, traiter à la main chaque image est très long, la deuxième année j'ai donc cherché à l'automatiser. La détection de visage est un dossier assez 0 la mode, il existe plein de choses sur internet. Mais je n'ai pas trouvé exactement ce qui allait me permettre de détecter le visage et redimensionner la photo pour le positionner toujours au même endroit sur chaque photo, je n'ai pas trouvé de logiciel tout fait qui faisait exactement ce qu'il me fallait. Par contre dans mes recherches sur internet j'ai remarqué que beaucoup de logiciels utilisaient OpenCV (Open Computer Vision). Je me suis lancé à faire un petit programme en python utilisant OpenCV qui allait faire ça pour moi. J'ai donc découvert cette librairie géniale qui m'a donné encore plus d'idées.

Le but est 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, ce que sait très bien faire OpenCV, j'ai choisi de coder avec Python (que je trouve plus pratique que C ou C++) sous Windows. Avec assez peu d'effort j'ai donc réussi à traiter toutes mes images rapidement.

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 au moment où j'ai commencé à coder cela (2015). Je choisis la version 32 bits (j'ai été bloqué en choisissant 64 bits à un moment donné). 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 à 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, pas la peine de chercher trop compliqué.

# 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, elle dépend de la résolution initiale de l'image par rapport au sujet.

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)

Amélioration supplémentaire

La première version de mon programme générait beaucoup de faux positifs. Ils étaient souvent écartés par mon système de filtrage mais de temps en temps ca posait problème. Entre temps j'ai eu une deuxième fille qui multiplie encore le nombre de photos mais me permet de rentabiliser deux fois plus mon programme ;-). Bref j'ai réussi à améliorer un peu mon programme. Je détecte aussi le visage. Comme expliqué initialement ca ne me donne pas des repères assez précis mais je l'utilise pour éliminer tous les faux positifs qui tombent hors de la position du visage détecté. Le programme s'en trouve plus rapide et plus précis.

Une autre amélioration consiste à tourner l'image tous les 45° si je ne trouve pas un visage sur l'image originale. En effet quand le visage est trop penché ca ne marchait pas très bien.