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.recognition;
031
032import java.io.DataInput;
033import java.io.DataOutput;
034import java.io.File;
035import java.io.IOException;
036import java.util.ArrayList;
037import java.util.List;
038import java.util.Set;
039
040import org.apache.log4j.Logger;
041import org.openimaj.data.dataset.GroupedDataset;
042import org.openimaj.data.dataset.ListDataset;
043import org.openimaj.feature.FeatureExtractor;
044import org.openimaj.image.FImage;
045import org.openimaj.image.processing.face.detection.DatasetFaceDetector;
046import org.openimaj.image.processing.face.detection.DetectedFace;
047import org.openimaj.image.processing.face.detection.FaceDetector;
048import org.openimaj.io.IOUtils;
049import org.openimaj.io.ReadWriteableBinary;
050import org.openimaj.ml.annotation.AnnotatedObject;
051import org.openimaj.ml.annotation.ScoredAnnotation;
052import org.openimaj.util.pair.IndependentPair;
053
054/**
055 * The {@link FaceRecognitionEngine} ties together the implementations of a
056 * {@link FaceDetector} and {@link FaceRecogniser}, and provides a single
057 * convenience API with which to interact with a face recognition system.
058 * 
059 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
060 * 
061 * @param <FACE>
062 *            Type of {@link DetectedFace}
063 * @param <PERSON>
064 *            Type representing a person
065 */
066public class FaceRecognitionEngine<FACE extends DetectedFace, PERSON>
067                implements
068                ReadWriteableBinary
069{
070        private static final Logger logger = Logger.getLogger(FaceRecognitionEngine.class);
071
072        protected FaceDetector<FACE, FImage> detector;
073        protected FaceRecogniser<FACE, PERSON> recogniser;
074
075        protected FaceRecognitionEngine() {
076        }
077
078        /**
079         * Construct a {@link FaceRecognitionEngine} with the given face detector
080         * and recogniser.
081         * 
082         * @param detector
083         *            the face detector
084         * @param recogniser
085         *            the face recogniser
086         */
087        public FaceRecognitionEngine(final FaceDetector<FACE, FImage> detector,
088                        final FaceRecogniser<FACE, PERSON> recogniser)
089        {
090                this.detector = detector;
091                this.recogniser = recogniser;
092        }
093
094        /**
095         * Create a {@link FaceRecognitionEngine} with the given face detector and
096         * recogniser.
097         * 
098         * @param <FACE>
099         *            Type of {@link DetectedFace}
100         * @param <EXTRACTOR>
101         *            Type of {@link FeatureExtractor}
102         * @param <PERSON>
103         *            Type representing a person
104         * 
105         * @param detector
106         *            the face detector
107         * @param recogniser
108         *            the face recogniser
109         * @return new {@link FaceRecognitionEngine}
110         */
111        public static <FACE extends DetectedFace, EXTRACTOR extends FeatureExtractor<?, FACE>, PERSON>
112                        FaceRecognitionEngine<FACE, PERSON>
113                        create(final FaceDetector<FACE, FImage> detector, final FaceRecogniser<FACE, PERSON> recogniser)
114        {
115                return new FaceRecognitionEngine<FACE, PERSON>(detector, recogniser);
116        }
117
118        /**
119         * @return the detector
120         */
121        public FaceDetector<FACE, FImage> getDetector() {
122                return this.detector;
123        }
124
125        /**
126         * @return the recogniser
127         */
128        public FaceRecogniser<FACE, PERSON> getRecogniser() {
129                return this.recogniser;
130        }
131
132        /**
133         * Save the {@link FaceRecognitionEngine} to a file, including all the
134         * internal state of the recogniser, etc.
135         * 
136         * @param file
137         *            the file to save to
138         * @throws IOException
139         *             if an error occurs when writing
140         */
141        public void save(final File file) throws IOException {
142                IOUtils.writeBinaryFull(file, this);
143        }
144
145        /**
146         * Load a {@link FaceRecognitionEngine} previously saved by
147         * {@link #save(File)}.
148         * 
149         * @param <O>
150         *            Type of {@link DetectedFace}
151         * @param <P>
152         *            Type representing a person
153         * 
154         * @param file
155         *            the file to read from
156         * @return the created recognition engine
157         * @throws IOException
158         *             if an error occurs during the read
159         */
160        public static <O extends DetectedFace, P> FaceRecognitionEngine<O, P> load(final File file) throws IOException
161        {
162                final FaceRecognitionEngine<O, P> engine = IOUtils.read(file);
163
164                return engine;
165        }
166
167        /**
168         * Train with a dataset
169         * 
170         * @param dataset
171         *            the dataset
172         */
173        public void train(final GroupedDataset<PERSON, ListDataset<FImage>, FImage> dataset) {
174                final GroupedDataset<PERSON, ListDataset<FACE>, FACE> faceDataset = DatasetFaceDetector
175                                .process(dataset, this.detector);
176                this.recogniser.train(faceDataset);
177        }
178
179        /**
180         * Train the recogniser with a single example, returning the detected face.
181         * If multiple faces are found, the biggest is chosen.
182         * <p>
183         * If you need more control, consider calling {@link #getDetector()} to get
184         * a detector which you can apply to your image and {@link #getRecogniser()}
185         * to get the recogniser which you can train with the detections directly.
186         * 
187         * @param person
188         *            the person
189         * @param image
190         *            the image with the persons face
191         * @return the detected face
192         */
193        public FACE train(final PERSON person, final FImage image) {
194                final List<FACE> faces = this.detector.detectFaces(image);
195
196                if (faces == null || faces.size() == 0) {
197                        FaceRecognitionEngine.logger.warn("no face detected");
198                        return null;
199                } else if (faces.size() == 1) {
200                        this.recogniser.train(AnnotatedObject.create(faces.get(0), person));
201                        return faces.get(0);
202                } else {
203                        FaceRecognitionEngine.logger.warn("More than one face found. Choosing biggest.");
204
205                        final FACE face = DatasetFaceDetector.getBiggest(faces);
206                        this.recogniser.train(AnnotatedObject.create(face, person));
207                        return face;
208                }
209        }
210
211        /**
212         * Train for the given face patch without doing any face detection. It is
213         * assumed that the given image will be a cropped/aligned image of the face
214         * as is necessary for the given recogniser.
215         * 
216         * @param face
217         *            The detected face implementation
218         * @param person
219         *            The person to whom this face belongs
220         * @return The face image
221         */
222        public FACE train(final FACE face, final PERSON person)
223        {
224                this.recogniser.train(AnnotatedObject.create(face, person));
225                return face;
226        }
227
228        /**
229         * Detect and recognise the faces in the given image, returning a list of
230         * potential people for each face.
231         * 
232         * @param image
233         *            the image
234         * @return a list of faces and recognitions
235         */
236        public List<IndependentPair<FACE, List<ScoredAnnotation<PERSON>>>> recognise(final FImage image) {
237                final List<FACE> detectedFaces = this.detector.detectFaces(image);
238                final List<IndependentPair<FACE, List<ScoredAnnotation<PERSON>>>> results = new ArrayList<IndependentPair<FACE, List<ScoredAnnotation<PERSON>>>>();
239
240                for (final FACE df : detectedFaces) {
241                        results.add(new IndependentPair<FACE, List<ScoredAnnotation<PERSON>>>(df, this.recogniser.annotate(df)));
242                }
243
244                return results;
245        }
246
247        /**
248         * Detect and recognise the faces in the given image, returning the most
249         * likely person for each face.
250         * 
251         * @param image
252         *            the image
253         * @return a list of faces with the most likely person
254         */
255        public List<IndependentPair<FACE, ScoredAnnotation<PERSON>>> recogniseBest(final FImage image) {
256                final List<FACE> detectedFaces = this.detector.detectFaces(image);
257                final List<IndependentPair<FACE, ScoredAnnotation<PERSON>>> results = new ArrayList<IndependentPair<FACE, ScoredAnnotation<PERSON>>>();
258
259                for (final FACE df : detectedFaces) {
260                        results.add(new IndependentPair<FACE, ScoredAnnotation<PERSON>>(df, this.recogniser.annotateBest(df)));
261                }
262
263                return results;
264        }
265
266        /**
267         * Detect and recognise the faces in the given image, returning a list of
268         * potential people for each face. The recognised people will be restricted
269         * to the given set.
270         * 
271         * @param image
272         *            the image
273         * @param restrict
274         *            set of people to restrict to
275         * @return a list of faces and recognitions
276         */
277        public List<IndependentPair<FACE, List<ScoredAnnotation<PERSON>>>> recognise(final FImage image,
278                        final Set<PERSON> restrict)
279        {
280                final List<FACE> detectedFaces = this.detector.detectFaces(image);
281                final List<IndependentPair<FACE, List<ScoredAnnotation<PERSON>>>> results = new ArrayList<IndependentPair<FACE, List<ScoredAnnotation<PERSON>>>>();
282
283                for (final FACE df : detectedFaces) {
284                        results.add(new IndependentPair<FACE, List<ScoredAnnotation<PERSON>>>(df, this.recogniser.annotate(df,
285                                        restrict)));
286                }
287
288                return results;
289        }
290
291        /**
292         * Detect and recognise the faces in the given image, returning the most
293         * likely person for each face. The recognised people will be restricted to
294         * the given set.
295         * 
296         * @param image
297         *            the image
298         * @param restrict
299         *            set of people to restrict to
300         * @return a list of faces with the most likely person
301         */
302        public List<IndependentPair<FACE, ScoredAnnotation<PERSON>>> recogniseBest(final FImage image,
303                        final Set<PERSON> restrict)
304        {
305                final List<FACE> detectedFaces = this.detector.detectFaces(image);
306                final List<IndependentPair<FACE, ScoredAnnotation<PERSON>>> results = new ArrayList<IndependentPair<FACE, ScoredAnnotation<PERSON>>>();
307
308                for (final FACE df : detectedFaces) {
309                        results.add(new IndependentPair<FACE, ScoredAnnotation<PERSON>>(df, this.recogniser
310                                        .annotateBest(df, restrict)));
311                }
312
313                return results;
314        }
315
316        @Override
317        public void readBinary(final DataInput in) throws IOException {
318                final String detectorClass = in.readUTF();
319                this.detector = IOUtils.newInstance(detectorClass);
320                this.detector.readBinary(in);
321
322                final String recogniserClass = in.readUTF();
323                this.recogniser = IOUtils.newInstance(recogniserClass);
324                this.recogniser.readBinary(in);
325        }
326
327        @Override
328        public byte[] binaryHeader() {
329                return "FaRE".getBytes();
330        }
331
332        @Override
333        public void writeBinary(final DataOutput out) throws IOException {
334                out.writeUTF(this.detector.getClass().getName());
335                this.detector.writeBinary(out);
336
337                out.writeUTF(this.recogniser.getClass().getName());
338                this.recogniser.writeBinary(out);
339        }
340}