001/** 002 * Copyright (c) 2011, The University of Southampton and the individual contributors. 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, 006 * are permitted provided that the following conditions are met: 007 * 008 * * Redistributions of source code must retain the above copyright notice, 009 * this list of conditions and the following disclaimer. 010 * 011 * * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * 015 * * Neither the name of the University of Southampton nor the names of its 016 * contributors may be used to endorse or promote products derived from this 017 * software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030package org.openimaj.image.processing.face.detection; 031 032import java.io.DataInput; 033import java.io.DataOutput; 034import java.io.File; 035import java.io.FileInputStream; 036import java.io.IOException; 037import java.io.InputStream; 038import java.io.ObjectInputStream; 039import java.io.ObjectOutputStream; 040import java.io.OutputStream; 041import java.util.ArrayList; 042import java.util.List; 043 044import org.openimaj.citation.annotation.Reference; 045import org.openimaj.citation.annotation.ReferenceType; 046import org.openimaj.image.FImage; 047import org.openimaj.image.objectdetection.filtering.DetectionFilter; 048import org.openimaj.image.objectdetection.filtering.OpenCVGrouping; 049import org.openimaj.image.objectdetection.haar.Detector; 050import org.openimaj.image.objectdetection.haar.OCVHaarLoader; 051import org.openimaj.image.objectdetection.haar.StageTreeClassifier; 052import org.openimaj.image.processing.algorithm.EqualisationProcessor; 053import org.openimaj.io.IOUtils; 054import org.openimaj.math.geometry.shape.Rectangle; 055import org.openimaj.util.hash.HashCodeUtil; 056import org.openimaj.util.pair.ObjectIntPair; 057 058/** 059 * A face detector based on a Haar cascade. The cascades provided by 060 * {@link BuiltInCascade} are the same as those available in OpenCV. 061 * 062 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 063 */ 064@Reference( 065 type = ReferenceType.Inproceedings, 066 author = { "Viola, P.", "Jones, M." }, 067 title = "Rapid object detection using a boosted cascade of simple features", 068 year = "2001", 069 booktitle = "Computer Vision and Pattern Recognition, 2001. CVPR 2001. Proceedings of the 2001 IEEE Computer Society Conference on", 070 pages = { " I", "511 ", " I", "518 vol.1" }, 071 number = "", 072 volume = "1", 073 customData = { 074 "keywords", 075 " AdaBoost; background regions; boosted simple feature cascade; classifiers; face detection; image processing; image representation; integral image; machine learning; object specific focus-of-attention mechanism; rapid object detection; real-time applications; statistical guarantees; visual object detection; feature extraction; image classification; image representation; learning (artificial intelligence); object detection;", 076 "doi", "10.1109/CVPR.2001.990517", 077 "ISSN", "1063-6919 " 078 }) 079public class HaarCascadeDetector implements FaceDetector<DetectedFace, FImage> { 080 /** 081 * The set of pre-trained cascades from OpenCV. 082 * 083 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 084 */ 085 public enum BuiltInCascade { 086 /** 087 * A eye detector 088 */ 089 eye("haarcascade_eye.xml"), 090 /** 091 * A eye with glasses detector 092 */ 093 eye_tree_eyeglasses("haarcascade_eye_tree_eyeglasses.xml"), 094 /** 095 * A frontal face detector 096 */ 097 frontalface_alt("haarcascade_frontalface_alt.xml"), 098 /** 099 * A frontal face detector 100 */ 101 frontalface_alt2("haarcascade_frontalface_alt2.xml"), 102 /** 103 * A frontal face detector 104 */ 105 frontalface_alt_tree("haarcascade_frontalface_alt_tree.xml"), 106 /** 107 * A frontal face detector 108 */ 109 frontalface_default("haarcascade_frontalface_default.xml"), 110 /** 111 * A fullbody detector 112 */ 113 fullbody("haarcascade_fullbody.xml"), 114 /** 115 * A left eye detector 116 */ 117 lefteye_2splits("haarcascade_lefteye_2splits.xml"), 118 /** 119 * A lower body detector 120 */ 121 lowerbody("haarcascade_lowerbody.xml"), 122 /** 123 * A detector for a pair of eyes 124 */ 125 mcs_eyepair_big("haarcascade_mcs_eyepair_big.xml"), 126 /** 127 * A detector for a pair of eyes 128 */ 129 mcs_eyepair_small("haarcascade_mcs_eyepair_small.xml"), 130 /** 131 * A left eye detector 132 */ 133 mcs_lefteye("haarcascade_mcs_lefteye.xml"), 134 /** 135 * A mouth detector 136 */ 137 mcs_mouth("haarcascade_mcs_mouth.xml"), 138 /** 139 * A nose detector 140 */ 141 mcs_nose("haarcascade_mcs_nose.xml"), 142 /** 143 * A right eye detector 144 */ 145 mcs_righteye("haarcascade_mcs_righteye.xml"), 146 /** 147 * An upper body detector 148 */ 149 mcs_upperbody("haarcascade_mcs_upperbody.xml"), 150 /** 151 * A profile face detector 152 */ 153 profileface("haarcascade_profileface.xml"), 154 /** 155 * A right eye detector 156 */ 157 righteye_2splits("haarcascade_righteye_2splits.xml"), 158 /** 159 * An upper body detector 160 */ 161 upperbody("haarcascade_upperbody.xml"); 162 163 private String classFile; 164 165 private BuiltInCascade(String classFile) { 166 this.classFile = classFile; 167 } 168 169 /** 170 * @return The name of the cascade resource 171 */ 172 public String classFile() { 173 return classFile; 174 } 175 176 /** 177 * Create a new detector with the this cascade. 178 * 179 * @return A new {@link HaarCascadeDetector} 180 */ 181 public HaarCascadeDetector load() { 182 try { 183 return new HaarCascadeDetector(classFile); 184 } catch (final Exception e) { 185 throw new RuntimeException(e); 186 } 187 } 188 } 189 190 protected Detector detector; 191 protected DetectionFilter<Rectangle, ObjectIntPair<Rectangle>> groupingFilter; 192 protected boolean histogramEqualize = false; 193 194 /** 195 * Construct with the given cascade resource. See 196 * {@link #setCascade(String)} to understand how the cascade is loaded. 197 * 198 * @param cas 199 * The cascade resource. 200 * @see #setCascade(String) 201 */ 202 public HaarCascadeDetector(String cas) { 203 try { 204 setCascade(cas); 205 } catch (final Exception e) { 206 throw new RuntimeException(e); 207 } 208 groupingFilter = new OpenCVGrouping(); 209 } 210 211 /** 212 * Construct with the {@link BuiltInCascade#frontalface_default} cascade. 213 */ 214 public HaarCascadeDetector() { 215 this(BuiltInCascade.frontalface_default.classFile()); 216 } 217 218 /** 219 * Construct with the {@link BuiltInCascade#frontalface_default} cascade and 220 * the given minimum search window size. 221 * 222 * @param minSize 223 * minimum search window size 224 */ 225 public HaarCascadeDetector(int minSize) { 226 this(); 227 this.detector.setMinimumDetectionSize(minSize); 228 } 229 230 /** 231 * Construct with the given cascade resource and the given minimum search 232 * window size. See {@link #setCascade(String)} to understand how the 233 * cascade is loaded. 234 * 235 * @param cas 236 * The cascade resource. 237 * @param minSize 238 * minimum search window size. 239 * 240 * @see #setCascade(String) 241 */ 242 public HaarCascadeDetector(String cas, int minSize) { 243 this(cas); 244 this.detector.setMinimumDetectionSize(minSize); 245 } 246 247 /** 248 * @return The minimum detection window size 249 */ 250 public int getMinSize() { 251 return this.detector.getMinimumDetectionSize(); 252 } 253 254 /** 255 * Set the minimum detection window size 256 * 257 * @param size 258 * the window size 259 */ 260 public void setMinSize(int size) { 261 this.detector.setMinimumDetectionSize(size); 262 } 263 264 /** 265 * @return The maximum detection window size 266 */ 267 public int getMaxSize() { 268 return this.detector.getMaximumDetectionSize(); 269 } 270 271 /** 272 * Set the maximum detection window size 273 * 274 * @param size 275 * the window size 276 */ 277 public void setMaxSize(int size) { 278 this.detector.setMaximumDetectionSize(size); 279 } 280 281 /** 282 * @return The grouping filter 283 */ 284 public DetectionFilter<Rectangle, ObjectIntPair<Rectangle>> getGroupingFilter() { 285 return groupingFilter; 286 } 287 288 /** 289 * Set the filter for merging detections 290 * 291 * @param grouping 292 */ 293 public void setGroupingFilter(DetectionFilter<Rectangle, ObjectIntPair<Rectangle>> grouping) { 294 this.groupingFilter = grouping; 295 } 296 297 @Override 298 public List<DetectedFace> detectFaces(FImage image) { 299 if (histogramEqualize) 300 image.processInplace(new EqualisationProcessor()); 301 302 final List<Rectangle> rects = detector.detect(image); 303 final List<ObjectIntPair<Rectangle>> filteredRects = groupingFilter.apply(rects); 304 305 final List<DetectedFace> results = new ArrayList<DetectedFace>(); 306 for (final ObjectIntPair<Rectangle> r : filteredRects) { 307 results.add(new DetectedFace(r.first, image.extractROI(r.first), r.second)); 308 } 309 310 return results; 311 } 312 313 /** 314 * @see Detector#getScaleFactor() 315 * @return The detector scale factor 316 */ 317 public double getScaleFactor() { 318 return detector.getScaleFactor(); 319 } 320 321 /** 322 * Set the cascade classifier for this detector. The cascade file is first 323 * searched for as a java resource, and if it is not found then a it is 324 * assumed to be a file on the filesystem. 325 * 326 * @param cascadeResource 327 * The cascade to load. 328 * @throws Exception 329 * if there is a problem loading the cascade. 330 */ 331 public void setCascade(String cascadeResource) throws Exception { 332 // try to load serialized cascade from external XML file 333 InputStream in = null; 334 try { 335 in = OCVHaarLoader.class.getResourceAsStream(cascadeResource); 336 337 if (in == null) { 338 in = new FileInputStream(new File(cascadeResource)); 339 } 340 final StageTreeClassifier cascade = OCVHaarLoader.read(in); 341 342 if (this.detector == null) 343 this.detector = new Detector(cascade); 344 else 345 this.detector = new Detector(cascade, this.detector.getScaleFactor()); 346 } catch (final Exception e) { 347 throw e; 348 } finally { 349 if (in != null) { 350 try { 351 in.close(); 352 } catch (final IOException e) { 353 } 354 } 355 } 356 } 357 358 /** 359 * Set the detector scale factor 360 * 361 * @see Detector#setScaleFactor(float) 362 * 363 * @param scaleFactor 364 * the scale factor 365 */ 366 public void setScale(float scaleFactor) { 367 this.detector.setScaleFactor(scaleFactor); 368 } 369 370 /** 371 * Serialize the detector using java serialization to the given stream 372 * 373 * @param os 374 * the stream 375 * @throws IOException 376 */ 377 public void save(OutputStream os) throws IOException { 378 final ObjectOutputStream oos = new ObjectOutputStream(os); 379 oos.writeObject(this); 380 } 381 382 /** 383 * Deserialize the detector from a stream. The detector must have been 384 * written with a previous invokation of {@link #save(OutputStream)}. 385 * 386 * @param is 387 * @return {@link HaarCascadeDetector} read from stream. 388 * @throws IOException 389 * @throws ClassNotFoundException 390 */ 391 public static HaarCascadeDetector read(InputStream is) throws IOException, ClassNotFoundException { 392 final ObjectInputStream ois = new ObjectInputStream(is); 393 return (HaarCascadeDetector) ois.readObject(); 394 } 395 396 @Override 397 public int hashCode() { 398 int hashCode = HashCodeUtil.SEED; 399 400 hashCode = HashCodeUtil.hash(hashCode, this.detector.getMinimumDetectionSize()); 401 hashCode = HashCodeUtil.hash(hashCode, this.detector.getScaleFactor()); 402 hashCode = HashCodeUtil.hash(hashCode, this.detector.getClassifier().getName()); 403 hashCode = HashCodeUtil.hash(hashCode, this.groupingFilter); 404 hashCode = HashCodeUtil.hash(hashCode, this.histogramEqualize); 405 406 return hashCode; 407 } 408 409 @Override 410 public void readBinary(DataInput in) throws IOException { 411 this.detector = IOUtils.read(in); 412 this.groupingFilter = IOUtils.read(in); 413 414 histogramEqualize = in.readBoolean(); 415 } 416 417 @Override 418 public byte[] binaryHeader() { 419 return "HAAR".getBytes(); 420 } 421 422 @Override 423 public void writeBinary(DataOutput out) throws IOException { 424 IOUtils.write(detector, out); 425 IOUtils.write(groupingFilter, out); 426 427 out.writeBoolean(histogramEqualize); 428 } 429 430 @Override 431 public String toString() { 432 return "HaarCascadeDetector[cascade=" + detector.getClassifier().getName() + "]"; 433 } 434 435 /** 436 * @return the underlying Haar cascade. 437 */ 438 public StageTreeClassifier getCascade() { 439 return detector.getClassifier(); 440 } 441 442 /** 443 * @return the underlying {@link Detector}. 444 */ 445 public Detector getDetector() { 446 return detector; 447 } 448}