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.math.geometry.shape.algorithm;
031
032import org.openimaj.math.geometry.point.Point2d;
033import org.openimaj.math.geometry.point.PointList;
034import org.openimaj.math.geometry.transforms.TransformUtilities;
035
036import Jama.Matrix;
037
038/**
039 * Ordinary Procrustes Analysis between two sets of corresponding
040 * {@link PointList}s. 
041 * 
042 * It is assumed that the number of points in the {@link PointList}s
043 * is equal, and that their is a one-to-one correspondance between
044 * the ith point in each list.
045 *  
046 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
047 *
048 */
049public class ProcrustesAnalysis {
050        protected PointList reference;
051        protected Point2d referenceCog;
052        protected double scaling;
053        protected boolean rotate = true;
054
055        /**
056         * Construct the {@link ProcrustesAnalysis} with the given
057         * reference shape.
058         * 
059         * @param reference The reference shape.
060         */
061        public ProcrustesAnalysis(PointList reference) {
062                this(reference, false);
063        }
064
065        /**
066         * Construct the {@link ProcrustesAnalysis} with the given
067         * reference shape. The reference shape is optionally normalised
068         * to a standardised scale and translated to the origin. 
069         * 
070         * @param reference The reference shape.
071         * @param normalise if true, then the reference is normalised (changing
072         *              the reference shape itself).
073         */
074        public ProcrustesAnalysis(PointList reference, boolean normalise) {
075                this.reference = reference;
076
077                referenceCog = reference.calculateCentroid();
078                scaling = computeScale(reference, referenceCog.getX(), referenceCog.getY());
079
080                if (normalise) {
081                        reference.translate(-referenceCog.getX(), -referenceCog.getY());
082                        reference.scale((float) scaling);
083
084                        referenceCog.setX(0);
085                        referenceCog.setY(0);
086                        scaling = 1;
087                }
088        }
089
090        protected static double computeScale(PointList pl, double tx, double ty) {
091                double scale = 0;
092
093                for (Point2d pt : pl) {
094                        double x = pt.getX() - tx;
095                        double y = pt.getY() - ty;
096
097                        scale += x*x + y*y;
098                }
099
100                scale = Math.sqrt(scale / pl.points.size());
101
102                return 1.0 / scale;
103        }
104
105        /**
106         * Align the given shape to the reference. The alignment
107         * happens inplace, so the input points are modified. The
108         * matrix used to transform the points is returned.
109         * 
110         * @param toAlign transform matrix
111         * @return aligned points (reference to toAlign)
112         * @throws IllegalArgumentException if the input is null or has a
113         *              different size to the reference.
114         */
115        public Matrix align(PointList toAlign) {
116                if (toAlign.points.size() != reference.points.size())
117                        throw new IllegalArgumentException("Point lists are different lengths");
118
119                //translation
120                Point2d cog = toAlign.calculateCentroid();
121                Matrix trans = TransformUtilities.translateToPointMatrix(cog, this.referenceCog);
122                toAlign.translate((float)trans.get(0,2), (float)trans.get(1,2));
123
124                //scaling
125                double scale = computeScale(toAlign, referenceCog.getX(), referenceCog.getY());
126                float sf = (float)(scale / this.scaling);
127                toAlign.scale(referenceCog, sf);
128
129                //rotation
130                double theta = 0;
131
132                if (rotate) {
133                        float num = 0;
134                        float den = 0;
135                        final int count = reference.points.size();
136
137                        for (int i=0; i<count; i++) {
138                                Point2d p1 = reference.points.get(i);
139                                Point2d p2 = toAlign.points.get(i);
140
141                                float p1x = (float) (p1.getX());
142                                float p1y = (float) (p1.getY());
143
144                                float p2x = (float) (p2.getX());
145                                float p2y = (float) (p2.getY());
146
147                                num += p2x*p1y - p2y*p1x;
148                                den += p2x*p1x + p2y*p1y;
149                        }
150
151                        theta = Math.atan2(num, den);
152
153                        toAlign.rotate(this.referenceCog, theta);
154                }
155
156                //compute matrix
157                Matrix scaleMat = TransformUtilities.scaleMatrixAboutPoint(sf, sf, this.referenceCog);
158                Matrix rotMat = TransformUtilities.rotationMatrixAboutPoint(theta, this.referenceCog.getX(), this.referenceCog.getY());
159                return rotMat.times(scaleMat).times(trans);
160        }
161
162        /**
163         * Compute the Procrustes Distance between two {@link PointList}s.
164         * If the distance is 0, then the shapes overlap exactly.
165         * 
166         * @param l1 first shape
167         * @param l2 second shape
168         * @return the calculated distance.
169         * @throws IllegalArgumentException if the input is null or has a
170         *              different size to the reference.
171         */
172        public static float computeProcrustesDistance(PointList l1, PointList l2) {
173                if (l1.points.size() != l2.points.size())
174                        throw new IllegalArgumentException("Point lists are different lengths");
175
176                final int count = l1.points.size();
177                float distance = 0;
178
179                for (int i=0; i<count; i++) {
180                        Point2d p1 = l1.points.get(i);
181                        Point2d p2 = l2.points.get(i);
182
183                        float dx = p1.getX() - p2.getX();
184                        float dy = p1.getY() - p2.getY();
185
186                        distance += dx*dx + dy*dy;
187                }
188
189                return (float) Math.sqrt(distance);
190        }
191}