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}