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