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}