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}