- March 27, 2012
- Posted by: Admin
- Category: PHP
Face Detection in PHP (without OpenCV).
We have 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 we expected to find many solutions for doing it with PHP. Surprisingly, the only one we 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 our case, we wanted to have a pure PHP solution, so it can work with most hosts.
Learning about face detection
So, we started to think about implementing it our self. We read a articles, scientific papers, etc. The website http://www.facedetection.com/ is a great resource by the way. From this short research, we learnt that one of the most popular solutions is to use Viola Jones training with a Haar classifier. Very informative, but tedious to implement.
Always look at what others are doing
Then we 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. We 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.
Once the code converted to PHP, here’s the result:
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 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 98 99 100 101 102 103 104 105 106 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 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 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 |
<?php class FaceDetector { 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"); } } public function faceDetect($file) { if (is_resource($file)) { $this->canvas = $file; } elseif (is_file($file)) { $this->canvas = imagecreatefromjpeg($file); } else { throw new Exception("Can not load $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->getImgStats($this->reduced_canvas); $this->face = $this->doDetectGreedyBigToSmall( $stats['ii'], $stats['ii2'], $stats['width'], $stats['height'] ); if ($this->face['w'] > 0) { $this->face['x'] *= $ratio; $this->face['y'] *= $ratio; $this->face['w'] *= $ratio; } } else { $stats = $this->getImgStats($this->canvas); $this->face = $this->doDetectGreedyBigToSmall( $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 json_encode($this->face); } public function getFace() { return $this->face; } protected function getImgStats($canvas) { $image_width = imagesx($canvas); $image_height = imagesy($canvas); $iis = $this->computeII($canvas, $image_width, $image_height); return array( 'width' => $image_width, 'height' => $image_height, 'ii' => $iis['ii'], 'ii2' => $iis['ii2'] ); } protected function computeII($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_h-1; $i++) { $ii[$i*$ii_w] = 0; $ii2[$i*$ii_w] = 0; $rowsum = 0; $rowsum2 = 0; for ($j=1; $j<$ii_w-1; $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 doDetectGreedyBigToSmall($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->detectOnSubImage($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 detectOnSubImage($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:
1 2 |
$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 🙂