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.alignment; 031 032import java.io.DataInput; 033import java.io.DataOutput; 034import java.io.IOException; 035 036import org.openimaj.image.FImage; 037import org.openimaj.image.ImageUtilities; 038import org.openimaj.image.processing.face.detection.keypoints.FKEFaceDetector; 039import org.openimaj.image.processing.face.detection.keypoints.FacialKeypoint; 040import org.openimaj.image.processing.face.detection.keypoints.KEDetectedFace; 041 042import Jama.Matrix; 043 044/** 045 * The {@link AffineAligner} attempts to find an affine transform that will warp 046 * the face to the canonical frame by aligning facial keypoints. 047 * 048 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 049 * 050 */ 051public class AffineAligner implements FaceAligner<KEDetectedFace> { 052 /** 053 * Normalised positions of facial parts 054 */ 055 protected final static float[][] Pmu = { 056 { 25.0347f, 34.1802f, 44.1943f, 53.4623f, 34.1208f, 39.3564f, 44.9156f, 31.1454f, 47.8747f }, 057 { 34.1580f, 34.1659f, 34.0936f, 33.8063f, 45.4179f, 47.0043f, 45.3628f, 53.0275f, 52.7999f } }; 058 059 final static int CANONICAL_SIZE = 80; 060 061 int facePatchWidth = 80; 062 int facePatchHeight = 80; 063 float facePatchBorderPercentage = 0.225f; 064 065 private FImage mask; 066 067 /** 068 * Default Constructor with the default mask (80x80) and default border 069 * percentage (0.225). 070 */ 071 public AffineAligner() { 072 this(loadDefaultMask()); 073 }; 074 075 /** 076 * Construct with a mask (in the canonical frame) to apply after aligning 077 * and default border percentage (0.225). 078 * 079 * @param mask 080 */ 081 public AffineAligner(FImage mask) { 082 this.mask = mask; 083 } 084 085 /** 086 * Construct with a mask (in the canonical frame) to apply after aligning 087 * and default border percentage (0.225). 088 * 089 * @param mask 090 * the mask 091 * @param facePatchBorderPercentage 092 * the proportional size (against the detection patch) of the 093 * border for the crop. Higher values result in a more zoomed-in 094 * face. 095 */ 096 public AffineAligner(FImage mask, float facePatchBorderPercentage) { 097 this.mask = mask; 098 this.facePatchBorderPercentage = facePatchBorderPercentage; 099 this.facePatchHeight = mask.height; 100 this.facePatchWidth = mask.width; 101 } 102 103 /** 104 * Construct with no mask and the given size and border. 105 * 106 * @param facePatchWidth 107 * the width of the desired aligned faces 108 * @param facePatchHeight 109 * the height of the desired aligned faces 110 * @param facePatchBorderPercentage 111 * the proportional size (against the detection patch) of the 112 * border for the crop. Higher values result in a more zoomed-in 113 * face. 114 */ 115 public AffineAligner(int facePatchWidth, int facePatchHeight, float facePatchBorderPercentage) { 116 this.mask = new FImage(facePatchWidth, facePatchHeight); 117 mask.fill(1f); 118 this.facePatchBorderPercentage = facePatchBorderPercentage; 119 this.facePatchWidth = facePatchWidth; 120 this.facePatchHeight = facePatchHeight; 121 } 122 123 @Override 124 public FImage align(KEDetectedFace descriptor) { 125 final int facePatchSize = Math.max(facePatchWidth, facePatchHeight); 126 final double size = facePatchSize + 2.0 * facePatchSize * facePatchBorderPercentage; 127 final double sc = CANONICAL_SIZE / size; 128 129 // do the scaling to everything but the translation!!! 130 final Matrix T = estimateAffineTransform(descriptor); 131 T.set(0, 0, T.get(0, 0) * sc); 132 T.set(1, 1, T.get(1, 1) * sc); 133 T.set(0, 1, T.get(0, 1) * sc); 134 T.set(1, 0, T.get(1, 0) * sc); 135 136 final FImage J = FKEFaceDetector.pyramidResize(descriptor.getFacePatch(), T); 137 final FImage bigPatch = FKEFaceDetector.extractPatch(J, T, (int) size, 138 (int) (facePatchSize * facePatchBorderPercentage)); 139 140 return bigPatch.extractCenter(facePatchWidth, facePatchHeight).extractROI(0, 0, facePatchWidth, facePatchHeight) 141 .multiplyInplace(mask); 142 } 143 144 /** 145 * Estimate the affine transform required to warp a set of facial keypoints 146 * to their canonical coordinates. 147 * <p> 148 * Affine transform is from a flat, vertically oriented (canonical) face to 149 * located face space. You'll need to invert this if you want to use it to 150 * extract the face from the image. 151 * 152 * @param face 153 * the face 154 * @return the affine transform matrix 155 */ 156 public static Matrix estimateAffineTransform(KEDetectedFace face) { 157 return estimateAffineTransform(face.getKeypoints()); 158 } 159 160 protected static Matrix estimateAffineTransform(FacialKeypoint[] pts) { 161 float emin = Float.POSITIVE_INFINITY; 162 Matrix T = null; 163 164 for (int c = 0; c < 9; c++) { 165 final Matrix A = new Matrix(8, 3); 166 final Matrix B = new Matrix(8, 3); 167 for (int i = 0, j = 0; i < 9; i++) { 168 if (i != 8 - c) { 169 A.set(j, 0, Pmu[0][i]); 170 A.set(j, 1, Pmu[1][i]); 171 A.set(j, 2, 1); 172 B.set(j, 0, pts[i].position.x); 173 B.set(j, 1, pts[i].position.y); 174 B.set(j, 2, 1); 175 j++; 176 } 177 } 178 179 final Matrix Tc = A.solve(B).transpose(); 180 181 final Matrix P1 = Tc.times(A.transpose()); 182 final Matrix D = P1.minus(B.transpose()); 183 184 float e = 0; 185 for (int cc = 0; cc < D.getColumnDimension(); cc++) { 186 float colsum = 0; 187 for (int rr = 0; rr < D.getRowDimension(); rr++) { 188 colsum += D.get(rr, cc) * D.get(rr, cc); 189 ; 190 } 191 e += Math.sqrt(colsum); 192 } 193 194 if (e < emin) { 195 emin = e; 196 T = Tc; 197 } 198 } 199 200 return T; 201 } 202 203 private static FImage loadDefaultMask() { 204 try { 205 return ImageUtilities.readF(FaceAligner.class.getResourceAsStream("affineMask.png")); 206 } catch (final IOException e) { 207 e.printStackTrace(); 208 } 209 210 return null; 211 } 212 213 @Override 214 public FImage getMask() { 215 return mask; 216 } 217 218 @Override 219 public void readBinary(DataInput in) throws IOException { 220 facePatchWidth = in.readInt(); 221 facePatchHeight = in.readInt(); 222 facePatchBorderPercentage = in.readFloat(); 223 mask = ImageUtilities.readF(in); 224 } 225 226 @Override 227 public byte[] binaryHeader() { 228 return this.getClass().getName().getBytes(); 229 } 230 231 @Override 232 public void writeBinary(DataOutput out) throws IOException { 233 out.writeInt(facePatchWidth); 234 out.writeInt(facePatchHeight); 235 out.writeFloat(facePatchBorderPercentage); 236 ImageUtilities.write(mask, "png", out); 237 } 238}