Face detection in pure PHP (without OpenCV)

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

Update: the code has moved to https://github.com/mauricesvay/php-facedetection

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:

Face detection 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!

67 replies on “Face detection in pure PHP (without OpenCV)”

  1. 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.

  2. 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).

  3. 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.

  4. 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…

  5. 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

  6. 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.

  7. 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.

  8. 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?

  9. 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.

  10. 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

  11. 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

  12. Octane: this implementation doesn’t support multiple faces.

    Rodolfo: depends on your picture resolution. Try to resize the image first.

  13. 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

  14. 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é !

  15. 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.

  16. 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

  17. 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!

  18. Could you be kind enough to add comments to the code for a beginner to understand with ease? Would appreciate.

  19. 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);

  20. 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);

  21. 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?

  22. Hi all,

    My name is Marco and I’m trying implement this script to example, but I have this error: ” Notice: imagecolorat() [function.imagecolorat]: 1,240 is out of bounds in C:\Program Files (x86)\EasyPHP5.2.10\www\asturcon\includes\php\detectFace.class.inc on line 118″ How I do now?. Thanks in advance and Congratulations for the article

  23. can you please help me
    i need a script for this
    photomontage ( if i created a template) i need to put the upload photos into template.

  24. Hi

    I’d like to detect a payment slip within a A4 TIFF image by a php or shell script and to copy it out.

    An example is here
    http://www.icefaescht.ch/img/ESR_44

    However, I am on reading articles, thesis and code but nothing has convinced me so far. As face recognition software is better documented, I was thinking about using a face recognizer software to recognize payment slips.

    Do you have experiences detecting shapes other than faces within pictures?

    regards

  25. Great concept and works pretty well for faces.

    As mentioned above, it would be great if we could train the .dat file. I am looking to use the system for adult content filtering and need to be able to work out how the serialised data has been structured or obtained.

    Any help on this would be greatly appreciated.

    Merci Bien

  26. I agree with the last two commenters. This is a great tutorial, but I would love more information on how to train the .dat file. Your classifiers don’t seem to detect dark skin (African) faces very well. Can you suggest how to train this to detect different colors of faces?

  27. Was recently working on this and started receiving several “out of bounds” and “offset” notice/warnings. Since I couldn’t fix these myself, and these are not errors (they are warnings and notices), I added this to the top of the class, and I was able to see this script successfully in action:

    error_reporting(E_ALL ^ (E_NOTICE | E_WARNING));
    set_time_limit(0);

  28. Hi, I have a problem, when I use the class it gives me the following error:
    Fatal error: Call to undefined function imagecreatefromjpeg() in /var/www/class_face.php on line 32
    I tried on my localhost, I don’t understand why is giving me the error, the file exist, the includes are ok, so I don’t know what’s going on! :S I hope you can help me.

  29. Thank’s for the sourcecode, i need this…but when i try to implement on mywebsite, there just appear some like this:

    ����JFIF��>CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), default quality ��C    $.’ “,#(7),01444’9=82<.342��C  2!!22222222222222222222222222222222222222222222222222����"�� ���}!1AQa"q2���#B��R��$3br� %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz��������������������������������������������������������������������������� ���w!1AQaq"2�B���� #3R�br� $4�%�&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz�������������������������������������������������������������������������� ?�!N�Q@ y�xk�:φ4�f�le�KX�ݭԳ��N95՞���V�2H[(�� �QNj���F��a�鶵�ޒ9?�5�2��֋��c� ~�� ���jW�}�����EQ��2O#,��+��� �m�ɲ7r��nv#���c�x�2�5#s�ں{�K�Q†I��� ���g�W0�3�=

    Thank’s for your attention sir

  30. just keep loading and displays the error has exceeded the execution after 30 seconds:

    in line:

    while( $current_node != null ){
    $vals = $current_node[0];
    $node_thresh = $vals[0];
    $leftval = $vals[1];
    $rightval = $vals[2];
    $leftidx = $vals[3];
    $rightidx = $vals[4];

  31. This error appers on the screen. Please help me out
    The image “localhost/face_detect/Face_detector.php” cannot be displayed, because it contains errors.

    Please look through it

  32. I’ve the same error, but only with some images. Try to comment $detector->toJpeg(); and see what type of error you have.
    In my case there is some error with Undefined index: x ….Undefined index: y….Undefined index: w….
    In function face_detect($file) on these lines:
    $this->face[‘x’] *= $ratio;
    $this->face[‘y’] *= $ratio;
    $this->face[‘w’] *= $ratio;
    For me it’s work only with some foto.

  33. It works fine with the face detection but I would like to know how i can do a face recognition with an existing set of pictures in my file/folder base of pictures. thanks in advance.

  34. Hey Maurice,

    Thank you very much for that very helpful piece of php code! 😉
    I had some issues with an endless loop at the do_detect_greedy_big_to_small-method, so I added a time limitation for the whole process.
    So I added:

    private $max_duration;

    and edited the _construct-method (new parameter and new line):


    public function __construct($detection_file = 'detection.dat',$max_duration=5) {
    if (is_file($detection_file)) {
    $this->detection_data = unserialize(file_get_contents($detection_file));
    } else {
    throw new Exception("Couldn't load detection data");
    }
    $this->max_duration = $max_duration; //<<<< NEW line!
    }

    …and in “do_detect_greedy_big_to_small”-method I added the following code and wrapped the loops into an if-condition:


    $startTimestamp = time(); <<< 1; $scale *= $scale_update ){
    $actualTimestamp = time(); //<<<< NEW line!
    if($actualTimestamp - $startTimestamp max_duration) { //<<<< NEW line!
    [... face detection ...]
    }
    else { //<<<< NEW line!
    break; //<<<< NEW line!
    } //<<<< NEW line!

    No never ending face detection any more 😉

    Greetings & have a nice day
    NEPOMUC

  35. I wonder is in PHP cause I liked this lenguage a lot, mainly it work on WEB giving beatiful portaibility of our codes… well.. have someone worked on this code getting a long?, I mean like the improvement of detection, localization of Eyes, mouth, nose, hair_line, low quality pics, parcial occlosing of the face, people with the eyes closed, etc..

    Thanks NEPOMUC for the “fix” ..

  36. Hola, a mi no me funciona el codigo, ya lo probe en tanto como linux como windows y no me funciona! Help me!, en linux simplemente no aparece nada, y en windows me decia del GD pero ya lo agregue y el tamaño de memoria, también ya lo edite y aun asi no me funciona.

  37. Excuse me sir, what inside of “detection.dat” based on your phpfacedetection? Is there adaboost, lbp or what. Sorry for my silly question.

  38. Hi there! Nice! Is it possible to detect if it’s male/female, child/adult, or other characteristics? that would be nice!

  39. Its working fine for one .. But How Can i detect more faces(its not working)
    thank you

  40. Hi,
    What are the data information that I have to save in MySQL to recognition after detect face in picture e to find and to show in my web application.

    Do you have some link about PHP FACE DETECTION using MySQL to storage data faces?

    Thanks

  41. Salut,

    Code nikel et prêt à l’emploi, nikel !
    Et comme tu voulais des idées, pourquoi pas proposer un détecteur de sourire ? 😉

Comments are closed.