Face detection in pure PHP (without OpenCV)
Par Maurice Svay le vendredi, juin 19 2009, 12:17 - Technologie - Lien permanent
Une résumé en français est disponible en fin d'article.
Lately, I've been looking for ways to detect faces in photos with PHP. Nowadays, face detection is built in many consumer products (camera obviously, but also Google and iPhoto), and seems to be a pretty common job. So I expected to find many solutions for doing it with PHP. Surprisingly, the only one I could find is OpenCV, an opensource lib that was originally developed by Intel. OpenCV seems to perform well but you need to be able to install it on your server. In my case, I wanted to have a pure PHP solution, so it can work with most hosts.
Learning about face detection
So I started to think about implementing it myself. I read a articles, scientific papers, etc. The website http://www.facedetection.com/ is a great resource by the way. From this short research, I learnt that one of the most popular solutions is to use Viola Jones training with a Haar classifier. Very informative, but tedious to implement. I'm lazy, you know.
Always look at what others are doing
Then I looked for existing implementations in other languages. Let's say Flash and Javascript. With Canvas, Javascript developers will certainly look at what flash developers do. There seem to be a few libs for face detection in AS3. They even work in real time. Pretty cool. I kept searching and finally found a canvas+javascript implementation of face detection at http://blog.kpicturebooth.com/?p=8. The code looked fairly compact and simple. Shouldn't be hard to port to PHP.
The code
Once the code converted to PHP, here's the result:
<?php
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// @Author Karthik Tharavaad
// karthik_tharavaad@yahoo.com
// @Contributor Maurice Svay
// maurice@svay.Com
class Face_Detector {
protected $detection_data;
protected $canvas;
protected $face;
private $reduced_canvas;
public function __construct($detection_file = 'detection.dat') {
if (is_file($detection_file)) {
$this->detection_data = unserialize(file_get_contents($detection_file));
} else {
throw new Exception("Couldn't load detection data");
}
//$this->detection_data = json_decode(file_get_contents('data.js'));
}
public function face_detect($file) {
if (!is_file($file)) {
throw new Exception("Can not load $file");
}
$this->canvas = imagecreatefromjpeg($file);
$im_width = imagesx($this->canvas);
$im_height = imagesy($this->canvas);
//Resample before detection?
$ratio = 0;
$diff_width = 320 - $im_width;
$diff_height = 240 - $im_height;
if ($diff_width > $diff_height) {
$ratio = $im_width / 320;
} else {
$ratio = $im_height / 240;
}
if ($ratio != 0) {
$this->reduced_canvas = imagecreatetruecolor($im_width / $ratio, $im_height / $ratio);
imagecopyresampled($this->reduced_canvas, $this->canvas, 0, 0, 0, 0, $im_width / $ratio, $im_height / $ratio, $im_width, $im_height);
$stats = $this->get_img_stats($this->reduced_canvas);
$this->face = $this->do_detect_greedy_big_to_small($stats['ii'], $stats['ii2'], $stats['width'], $stats['height']);
$this->face['x'] *= $ratio;
$this->face['y'] *= $ratio;
$this->face['w'] *= $ratio;
} else {
$stats = $this->get_img_stats($this->canvas);
$this->face = $this->do_detect_greedy_big_to_small($stats['ii'], $stats['ii2'], $stats['width'], $stats['height']);
}
return ($this->face['w'] > 0);
}
public function toJpeg() {
$color = imagecolorallocate($this->canvas, 255, 0, 0); //red
imagerectangle($this->canvas, $this->face['x'], $this->face['y'], $this->face['x']+$this->face['w'], $this->face['y']+ $this->face['w'], $color);
header('Content-type: image/jpeg');
imagejpeg($this->canvas);
}
public function toJson() {
return "{'x':" . $this->face['x'] . ", 'y':" . $this->face['y'] . ", 'w':" . $this->face['w'] . "}";
}
public function getFace() {
return $this->face;
}
protected function get_img_stats($canvas){
$image_width = imagesx($canvas);
$image_height = imagesy($canvas);
$iis = $this->compute_ii($canvas, $image_width, $image_height);
return array(
'width' => $image_width,
'height' => $image_height,
'ii' => $iis['ii'],
'ii2' => $iis['ii2']
);
}
protected function compute_ii($canvas, $image_width, $image_height ){
$ii_w = $image_width+1;
$ii_h = $image_height+1;
$ii = array();
$ii2 = array();
for($i=0; $i<$ii_w; $i++ ){
$ii[$i] = 0;
$ii2[$i] = 0;
}
for($i=1; $i<$ii_w; $i++ ){
$ii[$i*$ii_w] = 0;
$ii2[$i*$ii_w] = 0;
$rowsum = 0;
$rowsum2 = 0;
for($j=1; $j<$ii_h; $j++ ){
$rgb = ImageColorAt($canvas, $j, $i);
$red = ($rgb >> 16) & 0xFF;
$green = ($rgb >> 8) & 0xFF;
$blue = $rgb & 0xFF;
$grey = ( 0.2989*$red + 0.587*$green + 0.114*$blue )>>0; // this is what matlab uses
$rowsum += $grey;
$rowsum2 += $grey*$grey;
$ii_above = ($i-1)*$ii_w + $j;
$ii_this = $i*$ii_w + $j;
$ii[$ii_this] = $ii[$ii_above] + $rowsum;
$ii2[$ii_this] = $ii2[$ii_above] + $rowsum2;
}
}
return array('ii'=>$ii, 'ii2' => $ii2);
}
protected function do_detect_greedy_big_to_small( $ii, $ii2, $width, $height ){
$s_w = $width/20.0;
$s_h = $height/20.0;
$start_scale = $s_h < $s_w ? $s_h : $s_w;
$scale_update = 1 / 1.2;
for($scale = $start_scale; $scale > 1; $scale *= $scale_update ){
$w = (20*$scale) >> 0;
$endx = $width - $w - 1;
$endy = $height - $w - 1;
$step = max( $scale, 2 ) >> 0;
$inv_area = 1 / ($w*$w);
for($y = 0; $y < $endy ; $y += $step ){
for($x = 0; $x < $endx ; $x += $step ){
$passed = $this->detect_on_sub_image( $x, $y, $scale, $ii, $ii2, $w, $width+1, $inv_area);
if( $passed ) {
return array('x'=>$x, 'y'=>$y, 'w'=>$w);
}
} // end x
} // end y
} // end scale
return null;
}
protected function detect_on_sub_image( $x, $y, $scale, $ii, $ii2, $w, $iiw, $inv_area){
$mean = ( $ii[($y+$w)*$iiw + $x + $w] + $ii[$y*$iiw+$x] - $ii[($y+$w)*$iiw+$x] - $ii[$y*$iiw+$x+$w] )*$inv_area;
$vnorm = ( $ii2[($y+$w)*$iiw + $x + $w] + $ii2[$y*$iiw+$x] - $ii2[($y+$w)*$iiw+$x] - $ii2[$y*$iiw+$x+$w] )*$inv_area - ($mean*$mean);
$vnorm = $vnorm > 1 ? sqrt($vnorm) : 1;
$passed = true;
for($i_stage = 0; $i_stage < count($this->detection_data); $i_stage++ ){
$stage = $this->detection_data[$i_stage];
$trees = $stage[0];
$stage_thresh = $stage[1];
$stage_sum = 0;
for($i_tree = 0; $i_tree < count($trees); $i_tree++ ){
$tree = $trees[$i_tree];
$current_node = $tree[0];
$tree_sum = 0;
while( $current_node != null ){
$vals = $current_node[0];
$node_thresh = $vals[0];
$leftval = $vals[1];
$rightval = $vals[2];
$leftidx = $vals[3];
$rightidx = $vals[4];
$rects = $current_node[1];
$rect_sum = 0;
for( $i_rect = 0; $i_rect < count($rects); $i_rect++ ){
$s = $scale;
$rect = $rects[$i_rect];
$rx = ($rect[0]*$s+$x)>>0;
$ry = ($rect[1]*$s+$y)>>0;
$rw = ($rect[2]*$s)>>0;
$rh = ($rect[3]*$s)>>0;
$wt = $rect[4];
$r_sum = ( $ii[($ry+$rh)*$iiw + $rx + $rw] + $ii[$ry*$iiw+$rx] - $ii[($ry+$rh)*$iiw+$rx] - $ii[$ry*$iiw+$rx+$rw] )*$wt;
$rect_sum += $r_sum;
}
$rect_sum *= $inv_area;
$current_node = null;
if( $rect_sum >= $node_thresh*$vnorm ){
if( $rightidx == -1 )
$tree_sum = $rightval;
else
$current_node = $tree[$rightidx];
} else {
if( $leftidx == -1 )
$tree_sum = $leftval;
else
$current_node = $tree[$leftidx];
}
}
$stage_sum += $tree_sum;
}
if( $stage_sum < $stage_thresh ){
return false;
}
}
return true;
}
}
And you simply use the class this way:
$detector = new Face_Detector('detection.dat');
$detector->face_detect('maurice_svay_150.jpg');
$detector->toJpeg();
Which gives the following result:

The code requires GD and is a bit slow but should work on most PHP servers. You'll also need the data file: http://svay.com/experiences/face-detection/detection.dat. Let me know if you have ideas for improving the code :)
Résumé en français
J'ai récemment cherché une solution pour faire de la détection de visage en PHP. Après avec écarté plusieurs solutions qui ne sont pas facilement utilisable sur la plupart des hébergeurs, j'ai finalement porté du code Javascript+Canvas vers PHP+GD. Le résultat une class PHP qui est un peu lente, mais qui fonctionne sur la plupart des serveurs PHP. Si vous avez des idées pour améliorer le code, n'hésitez-pas!
Commentaires
Impressionnant travail ! J'adore tes articles techniques.
Pas mal comme idée de dév !
Sympa.
Mais ça fonctionne pas chez moi :)
Avec une image jpg de 188 * 264 px, j'ai une boucle infinie.
Avec, tout d'abord, cette erreur une bonne centaine de fois :
Notice: imagecolorat() [function.imagecolorat]: 325,1 is out of bounds in ****\face.php on line 121
Et le 325 qui s'incrémente.
La ligne 121 est :
$rgb = ImageColorAt($canvas, $j, $i);
Puis de manière infinie, celle-ci :
Notice: Undefined offset: 113445 in ***\face.php on line 198
Et le 113445 qui s'incrémente.
La ligne 198 est :
$r_sum = ( $ii[($ry+$rh)*$iiw + $rx + $rw] + $ii[$ry*$iiw+$rx] - $ii[($ry+$rh)*$iiw+$rx] - $ii[$ry*$iiw+$rx+$rw] )*$wt;
J'ai uniquement ajouté un set_time_limit(0) au début du script.
Si j'utilise le détecteur javascript, il me dit qu'aucune face n'a pu être detectée (alors qu'il y en a une. C'est ma photo de profil facebook).
J'ai mis une démo qui tourne là: http://svay.com/experiences/face-de... avec le code source:
http://svay.com/experiences/face-de...
Y'a peut être des problèmes quand ils ne trouve pas de visage cela dit... mais ça, j'en sais rien. Je n'ai fait que porter le code :)
Wow, impressionnant ! Je garde ça sous le coude, je suis persuadé que ça me servira un jour. :)
Excellent travail Maurice, je jsuis de meme a la recherche d'un algo pratique pour detection de visage et de nudite. Si on veut tourner ca sur un site social, ca sera pas du tout leger sur les serveurs.
Hatem: pour une utilisation intensive, je pense que OpenCV est une meilleure solution. Surtout si tu as la main sur le serveur qui héberge ton site social.
Concernant la nudité, je ne me suis pas penché sur la question. Mais ça ne me semble pas simple...
also same error on windows viste with php 5.2.10
Notice: imagecolorat() [function.imagecolorat]: 1,240 is out of bounds in C:\server\www\faceoff\Face_Detector.php on line 118
One such optimisation is very simply to store the data as a php array in a php file for inclusion, instead of having a serialized text file. Use var_export to export the data contained in the .dat file.
Great!
Next step: Smile detector
A good implementation. But...
This is Viola and Jones classifier.
Only the training algorithm was left out but its results are into detection.dat file.
In time, OpenCV algorithm for face detection is too a implementation of Viola and Jones algorithm.
Great code. But I have a question. Just to spped things up, when I replace 320 with 160 and 240 with 120 (or $ratio*2), results change, sometimes slightly, sometimes drastically. Is there a way to fix this?
la détection de peau et de visages sont des sujets de recherches très actifs en traitement d'image. Les méthodes les plus performantes doivent être celles basées sur un apprentissage, donc ca peut être assez lourd.
Je connais pas les possibilités / performances de PHP sur ce genre de choses, par contre pour OpenCV c'est une bonne librairie avec plein de choses toutes faites, et même un wrapper python, ce qui peut probablement être intéressant pour une appli web.
This is Viola-Jones implementation from OpenCV. The code is 100% the same.
how to detect muit-faces?
Super intéressant, j'adore tes articles techniques, mais malheureusement je n'ai jamais le temps de d'inspecter ton code en profondeur pour proposer des améliorations. Promis, pendant tes vacances je m'y met
hi, i'm from brazil .. thanks for tutorial
My error
Fatal error: Allowed memory size of 12582912 bytes exhausted (tried to allocate 35 bytes) in C:\AppServ\www\index.php on line 130
sorry my english
memory_limit = 50M ?
or
memory_limit = 25M ?
Octane: this implementation doesn't support multiple faces.
Rodolfo: depends on your picture resolution. Try to resize the image first.
thanks, and
how to detect on video?
sorry my english
as iam researching on face detection i found this script and it is very useful to me to get solution in my project, but here i got little bit problem , i want the eye and mouth positions,
can we even get these position in a picture using this technique, if anyone help to get me this point , i will be glad, thanks in advance.
please help give me some ideas to detect eyes and mouth in a photo
Pour une utilisation intensive, sous la forme d'un tutorial, le site Corpocrat Blog nous montre la facilité de réaliser l'opération avec la libraire OpenCV et l'extension PHP Facedetect.
Plus d'informations :
http://corpocrat.com/2009/08/18/aut...
Merci pour ce remarquable travail. Je cherchais un moyen de recadrer des photos qui doivent avoir toutes un même format de sortie à partir de formats variés, de préférence évidemment autour de la tête du sujet.
Grâce à vous mon boulot est bien avancé !
Hey,
I read your article and i think it's realy good, but if you have more then one face on an image, it doesn't work anymore. Although i wrote an article on my blog about it: http://www.web-schnipsel.de/2009/al...
Génial! Merci pour le code!
Faire ça en PHP n'est vraiment pas efficace! C'est pas le langage indiqué pour ça.
PHP is not the appropriate language to do face detection. First reason : very slow.
I try this code, it is perfect, but how can I train or get my own detection.dat file.
Thanks in advance!
Hello Thanks for the code. It looks good
I was working on a project and I wanted the face detected and extracted. I need only the face out ... Do you have any ideas what I could use..
Thanks a bunch
warm regards
Vandana
I was wondering about the data.js used in the commented part of the __construct function. Do you have this file? Can you send it to me?
Thanks in advance!
baa, my experiment was this, :(.. Ill be implementing fast in something
Could you be kind enough to add comments to the code for a beginner to understand with ease? Would appreciate.
Apparemment, le script ne fonctionne qu'avec une image carrée.
remplacer le code par ces quelques lignes devrait faire l'affaire (vers la ligne 57) :
$size_img = $im_width / $ratio;
if($im_height / $ratio>$size_img) $size_img = $im_height / $ratio;
if ($ratio != 0) {
$this->reduced_canvas = imagecreatetruecolor($size_img, $size_img);
petite correction :
$size_img = $im_width / $ratio;
if($im_height / $ratio<$size_img) $size_img = $im_height / $ratio;
if ($ratio != 0) {
$this->reduced_canvas = imagecreatetruecolor($size_img, $size_img);
I would like to know whether this code can count the number of faces or not
Thank you in advance :)
I tried using it and it kept on calculating an image to 320 x 200 even though I used a square image.
It complains about line 118 about out of bound because of imagecolorat.
anybody has a working version of this code?
Is there a Face Detection with pure C ?