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.feature;
031
032import java.io.DataInput;
033import java.io.DataOutput;
034import java.io.IOException;
035import java.util.ArrayList;
036import java.util.List;
037
038import org.openimaj.feature.FeatureVectorProvider;
039import org.openimaj.feature.FloatFV;
040import org.openimaj.image.FImage;
041import org.openimaj.image.pixel.Pixel;
042import org.openimaj.image.processing.face.alignment.AffineAligner;
043import org.openimaj.image.processing.face.detection.keypoints.FKEFaceDetector;
044import org.openimaj.image.processing.face.detection.keypoints.FacialKeypoint;
045import org.openimaj.image.processing.face.detection.keypoints.KEDetectedFace;
046import org.openimaj.image.processing.face.detection.keypoints.FacialKeypoint.FacialKeypointType;
047import org.openimaj.io.ReadWriteableBinary;
048import org.openimaj.io.wrappers.ReadableListBinary;
049import org.openimaj.io.wrappers.WriteableListBinary;
050import org.openimaj.math.geometry.point.Point2d;
051import org.openimaj.math.geometry.point.Point2dImpl;
052
053import Jama.Matrix;
054
055/**
056 * A {@link FacialFeature} that is built by concatenating
057 * each of the normalised facial part patches from a detected
058 * face. 
059 * 
060 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
061 *
062 */
063public class FacePatchFeature implements FacialFeature, FeatureVectorProvider<FloatFV> {
064        /**
065         * A {@link FacialFeatureExtractor} for producing {@link FacialFeature}s
066         * 
067         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
068         *
069         */
070        public static class Extractor implements FacialFeatureExtractor<FacePatchFeature, KEDetectedFace> {
071                /**
072                 * Default constructor
073                 */
074                public Extractor() {}
075                
076                @Override
077                public FacePatchFeature extractFeature(KEDetectedFace face) {
078                        FacePatchFeature f = new FacePatchFeature();
079                        f.initialise(face);
080                        return f;
081                }
082
083                @Override
084                public void readBinary(DataInput in) throws IOException {
085                        //Do nothing
086                }
087
088                @Override
089                public byte[] binaryHeader() {
090                        //Do nothing
091                        return null;
092                }
093
094                @Override
095                public void writeBinary(DataOutput out) throws IOException {
096                        //Do nothing
097                }
098        }
099        
100        /**
101         * A {@link FacialKeypoint} with an associated feature
102         * 
103         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
104         */
105        public static class DetectedFacePart extends FacialKeypoint implements ReadWriteableBinary {
106                float [] featureVector;
107                int featureRadius;
108                
109                /**
110                 * Default constructor
111                 */
112                public DetectedFacePart() {
113                        super();
114                }
115                
116                /**
117                 * Construct with the given parameters
118                 * @param type the type of keypoint
119                 * @param position the position of the keypoint
120                 */
121                public DetectedFacePart(FacialKeypointType type, Point2d position) {
122                        super(type, position);
123                }
124                
125                /**
126                 * @return the image patch around the keypoint
127                 */
128                public FImage getImage() {
129                        FImage image = new FImage(2*featureRadius+1,2*featureRadius+1);
130                        
131                        for (int i=0, rr=-featureRadius; rr<=featureRadius; rr++) {
132                                for (int cc=-featureRadius; cc<=featureRadius; cc++) {
133                                        float r2 = rr*rr + cc*cc;
134                                        
135                                        if (r2<=featureRadius*featureRadius) { //inside circle
136                                                float value = featureVector[i++];
137                                                
138                                                image.pixels[rr + featureRadius][cc + featureRadius] = value < -3 ? 0 : value >=3 ? 1 : (3f + value) / 6f;  
139                                        }
140                                }
141                        }
142                        
143                        return image;
144                }
145
146                @Override
147                public void readBinary(DataInput in) throws IOException {
148                        super.readBinary(in);
149                        
150                        int sz = in.readInt();
151                        if (sz<0) {
152                                featureVector = null;
153                        } else {
154                                featureVector = new float[sz];
155                                for (int i=0; i<sz; i++) featureVector[i] = in.readFloat();
156                        }
157                        
158                        featureRadius = in.readInt();
159                }
160
161                @Override
162                public byte[] binaryHeader() {
163                        return this.getClass().getName().getBytes();
164                }
165
166                @Override
167                public void writeBinary(DataOutput out) throws IOException {
168                        super.writeBinary(out);
169                        
170                        if (featureVector == null) {
171                                out.writeInt(-1);
172                        } else {
173                                out.writeInt(featureVector.length);
174                                for (float f : featureVector) out.writeFloat(f);
175                        }
176                        
177                        out.writeInt(featureRadius);
178                }
179        }
180        
181        final static int [][] VP = {
182                {0}, // EYE_LEFT_LEFT, 
183                {1}, // EYE_LEFT_RIGHT,
184                {2}, // EYE_RIGHT_LEFT,
185                {3}, // EYE_RIGHT_RIGHT,
186                {4}, // NOSE_LEFT,
187                {5}, // NOSE_MIDDLE,
188                {6}, // NOSE_RIGHT,
189                {7}, // MOUTH_LEFT,
190                {8}, // MOUTH_RIGHT,
191                {0, 1}, //      EYE_LEFT_CENTER,
192                {2, 3}, //      EYE_RIGHT_CENTER,
193                {1, 2}, //      NOSE_BRIDGE,
194                {7, 8}}; //     MOUTH_CENTER
195        
196        protected FloatFV featureVector;
197        
198        /** The radius of the descriptor samples about each point */
199        protected int radius = 7;
200        
201        /** The scale of the descriptor samples about each point */
202        protected float scl = 1;
203        
204        protected List<DetectedFacePart> faceParts = new ArrayList<DetectedFacePart>();
205
206        /**
207         * Default constructor.
208         */
209        public FacePatchFeature() {
210        }
211        
212        protected void initialise(KEDetectedFace face) {
213                extractFeatures(face);
214                this.featureVector = createFeatureVector();
215        }
216
217        protected FloatFV createFeatureVector() {
218                int length = faceParts.get(0).featureVector.length;
219                FloatFV fv = new FloatFV(faceParts.size() * length);
220                
221                for (int i=0; i<faceParts.size(); i++) {
222                        System.arraycopy(faceParts.get(i).featureVector, 0, fv.values, i*length, length);
223                }
224                
225                return fv;
226        }
227                
228        protected void extractFeatures(KEDetectedFace face) {
229                Matrix T0 = AffineAligner.estimateAffineTransform(face);
230                Matrix T = T0.copy();
231                FImage J = FKEFaceDetector.pyramidResize(face.getFacePatch(), T);
232                FacialKeypoint[] pts = face.getKeypoints();
233                faceParts.clear();
234                
235                float pyrScale = (float) (T0.get(0,2) / T.get(0, 2));
236                
237                //build a list of the center of each patch wrt image J
238                Point2dImpl[] P0 = new Point2dImpl[VP.length];
239                for (int j=0; j<P0.length; j++) {
240                        int [] vp = VP[j];
241                        int vp0 = vp[0];
242                        
243                        P0[j] = new Point2dImpl(0, 0);
244                        if (vp.length == 1) {
245                                P0[j].x = pts[vp0].position.x / pyrScale;
246                                P0[j].y = pts[vp0].position.y / pyrScale;
247                        } else {
248                                int vp1 = vp[1];
249                                P0[j].x = ((pts[vp0].position.x + pts[vp1].position.x) / 2.0f) / pyrScale;
250                                P0[j].y = ((pts[vp0].position.y + pts[vp1].position.y) / 2.0f) / pyrScale;
251                        }
252                }
253                
254                //Prebuild transform
255                List<Point2dImpl> transformed = new ArrayList<Point2dImpl>();
256                List<Pixel> nontransformed = new ArrayList<Pixel>();
257                for (int rr=-radius; rr<=radius; rr++) {
258                        for (int cc=-radius; cc<=radius; cc++) {
259                                float r2 = rr*rr + cc*cc;
260                                if (r2<=radius*radius) { //inside circle
261                                        //Note: do transform without the translation!!!
262                                        float px = (float) (cc*scl* T.get(0, 0) + rr*scl*T.get(0, 1));
263                                        float py = (float) (cc*scl* T.get(1, 0) + rr*scl*T.get(1, 1));
264                                        
265                                        transformed.add(new Point2dImpl(px, py));
266                                        nontransformed.add(new Pixel(cc,rr));
267                                }
268                        }
269                }
270                
271                for (int j=0; j<VP.length; j++) {
272                        DetectedFacePart pd = new DetectedFacePart(FacialKeypointType.valueOf(j), new Point2dImpl(P0[j].x * pyrScale, P0[j].y * pyrScale));
273                        faceParts.add(pd);
274                        pd.featureVector = new float[transformed.size()];
275                        
276                        int n = 0;
277                        float mean = 0;
278                        float m2 = 0;
279                        
280                        for (int i=0; i<transformed.size(); i++) {
281                                Point2dImpl XYt = transformed.get(i);
282                                
283                                double xt = XYt.x + P0[j].x;
284                                double yt = XYt.y + P0[j].y;
285                                float val = J.getPixelInterp(xt, yt);
286                                
287                                pd.featureVector[i] = val;
288                                
289                                n++;
290                                float delta = val - mean;
291                                mean = mean + delta / n;
292                                m2 = m2 + delta*(val - mean);
293                        }
294                        
295                        float std = (float) Math.sqrt(m2 / (n-1));
296                        if (std <= 0) std = 1;
297                        
298                        for (int i=0; i<transformed.size(); i++) {
299                                pd.featureVector[i] = (pd.featureVector[i] - mean) / std;
300                        }
301                }
302        }
303
304        @Override
305        public FloatFV getFeatureVector() {
306                return this.featureVector;
307        }
308
309        @Override
310        public void readBinary(DataInput in) throws IOException {
311                featureVector = new FloatFV();
312                featureVector.readBinary(in);
313                
314                radius = in.readInt();
315                scl = in.readFloat();
316                
317                new ReadableListBinary<DetectedFacePart>(faceParts) {
318                        @Override
319                        protected DetectedFacePart readValue(DataInput in) throws IOException {
320                                DetectedFacePart v = new DetectedFacePart();
321                                v.readBinary(in);
322                                return v;
323                        }
324                }.readBinary(in);
325        }
326
327        @Override
328        public byte[] binaryHeader() {
329                return this.getClass().getName().getBytes();
330        }
331
332        @Override
333        public void writeBinary(DataOutput out) throws IOException {
334                featureVector.writeBinary(out);
335                out.writeInt(radius);
336                out.writeFloat(scl);
337                
338                new WriteableListBinary<DetectedFacePart>(faceParts) {
339                        @Override
340                        protected void writeValue(DetectedFacePart v, DataOutput out) throws IOException {
341                                v.writeBinary(out);
342                        }
343                }.writeBinary(out);
344        }
345}