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.FacialKeypoint.FacialKeypointType;
041import org.openimaj.image.processing.face.detection.keypoints.KEDetectedFace;
042import org.openimaj.math.geometry.transforms.TransformUtilities;
043
044import Jama.Matrix;
045
046/**
047 * Attempt to align a face by rotating and scaling it. Facial Keypoints are used
048 * to judge the alignment. Specifically, the distance between the eyes is
049 * normalised by scaling, and the eyes are rotated to be level. The face is then
050 * translated to a known position (again, based on the eyes).
051 * 
052 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
053 * 
054 */
055public class RotateScaleAligner implements FaceAligner<KEDetectedFace> {
056        private static final FImage DEFAULT_MASK = loadDefaultMask();
057
058        // Define the geometry
059        private int eyeDist = 68;
060        private int eyePaddingLeftRight = 6;
061        private int eyePaddingTop = 20;
062
063        private FImage mask = DEFAULT_MASK;
064
065        /**
066         * Default constructor with no mask.
067         */
068        public RotateScaleAligner() {
069        }
070
071        /**
072         * Default constructor with no mask.
073         * 
074         * @param targetSize
075         *            target aligned image size
076         */
077        public RotateScaleAligner(int targetSize) {
078                final int canonicalSize = 2 * eyePaddingLeftRight + eyeDist;
079
080                final double sf = targetSize / canonicalSize;
081
082                eyeDist = (int) (eyeDist * sf);
083                eyePaddingLeftRight = (targetSize - eyeDist) / 2;
084                eyePaddingTop = (int) (eyePaddingTop * sf);
085        }
086
087        /**
088         * Construct with a mask (in the canonical frame) to apply after alignment.
089         * 
090         * @param mask
091         *            The mask.
092         */
093        public RotateScaleAligner(FImage mask) {
094                this.mask = mask;
095        }
096
097        @Override
098        public FImage align(KEDetectedFace descriptor) {
099                final FacialKeypoint lefteye = descriptor.getKeypoint(FacialKeypointType.EYE_LEFT_LEFT);
100                final FacialKeypoint righteye = descriptor.getKeypoint(FacialKeypointType.EYE_RIGHT_RIGHT);
101
102                final float dx = righteye.position.x - lefteye.position.x;
103                final float dy = righteye.position.y - lefteye.position.y;
104
105                final float rotation = (float) Math.atan2(dy, dx);
106                final float scaling = (float) (eyeDist / Math.sqrt(dx * dx + dy * dy));
107
108                final float tx = lefteye.position.x - eyePaddingLeftRight / scaling;
109                final float ty = lefteye.position.y - eyePaddingTop / scaling;
110
111                final Matrix tf0 = TransformUtilities.scaleMatrix(scaling, scaling)
112                                .times(TransformUtilities.translateMatrix(-tx, -ty))
113                                .times(TransformUtilities.rotationMatrixAboutPoint(-rotation, lefteye.position.x, lefteye.position.y));
114                final Matrix tf = tf0.inverse();
115
116                final FImage J = FKEFaceDetector.pyramidResize(descriptor.getFacePatch(), tf);
117                return FKEFaceDetector.extractPatch(J, tf, 2 * eyePaddingLeftRight + eyeDist, 0);
118        }
119
120        private static FImage loadDefaultMask() {
121                try {
122                        return ImageUtilities.readF(FaceAligner.class.getResourceAsStream("affineMask.png"));
123                } catch (final IOException e) {
124                        e.printStackTrace();
125                }
126
127                return null;
128        }
129
130        @Override
131        public FImage getMask() {
132                return mask;
133        }
134
135        @Override
136        public void readBinary(DataInput in) throws IOException {
137                eyeDist = in.readInt();
138                eyePaddingLeftRight = in.readInt();
139                eyePaddingTop = in.readInt();
140
141                mask = ImageUtilities.readF(in);
142        }
143
144        @Override
145        public byte[] binaryHeader() {
146                return this.getClass().getName().getBytes();
147        }
148
149        @Override
150        public void writeBinary(DataOutput out) throws IOException {
151                out.writeInt(eyeDist);
152                out.writeInt(eyePaddingLeftRight);
153                out.writeInt(eyePaddingTop);
154
155                ImageUtilities.write(mask, "png", out);
156        }
157}