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;
031
032import java.util.ArrayList;
033import java.util.List;
034
035import org.openimaj.citation.annotation.Reference;
036import org.openimaj.citation.annotation.ReferenceType;
037import org.openimaj.citation.annotation.References;
038import org.openimaj.math.geometry.point.Point2d;
039import org.openimaj.math.geometry.point.Point2dImpl;
040import org.openimaj.math.geometry.shape.algorithm.GeneralisedProcrustesAnalysis;
041import org.openimaj.math.geometry.shape.algorithm.ProcrustesAnalysis;
042import org.openimaj.math.matrix.algorithm.pca.PrincipalComponentAnalysis;
043import org.openimaj.math.matrix.algorithm.pca.PrincipalComponentAnalysis.ComponentSelector;
044import org.openimaj.math.matrix.algorithm.pca.SvdPrincipalComponentAnalysis;
045import org.openimaj.util.pair.IndependentPair;
046
047import Jama.Matrix;
048
049/**
050 * A 2d point distribution model learnt from a set of {@link PointList}s with
051 * corresponding points (the ith point in each {@link PointList} is the same
052 * landmark).
053 * 
054 * The pdm models the mean shape and the variance from the mean of the
055 * top N principal components. The model is generative and can generate new
056 * shapes from a scaling vector. To ensure that newly generated shapes are 
057 * plausible, scaling vectors have {@link Constraint}s applied to them. 
058 * 
059 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
060 *
061 */
062@References(references = { 
063                @Reference(
064                                author = { "Cootes, T. F.", "Taylor, C. J." }, 
065                                title = "Statistical Models of Appearance for Computer Vision", 
066                                type = ReferenceType.Unpublished,
067                                month = "October",
068                                year = "2001",
069                                url = "http://isbe.man.ac.uk/~bim/Models/app_model.ps.gz"
070                ), 
071                @Reference(
072                                type = ReferenceType.Inproceedings,
073                                author = { "Cj. Taylor", "D. H. Cooper", "J. Graham" },
074                                title = "Training models of shape from sets of examples",
075                                year = "1992",
076                                booktitle = "Proc. BMVC92, Springer-Verlag",
077                                pages = { "9", "", "18" }
078                )
079})
080public class PointDistributionModel {
081        /**
082         * Interface for modelling constraints applied to the
083         * scaling vector of {@link PointDistributionModel}s
084         * so that generated models are plausible.
085         * 
086         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
087         */
088        public interface Constraint {
089                /**
090                 * Apply constraints to a scaling vector so that it
091                 * will generated a plausible model and return the 
092                 * new constrained vector.
093                 * @param scaling the scaling vector to constrain
094                 * @param lamda the eigenvalues of the {@link PointDistributionModel}
095                 * @return the constrained scaling vector
096                 */
097                public double [] apply(double [] scaling, double [] lamda);
098        }
099        
100        /**
101         * A constraint that does nothing. 
102         * 
103         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
104         *
105         */
106        public static class NullConstraint implements Constraint {
107                @Override
108                public double[] apply(double[] in, double [] lamda) {
109                        return in;
110                }
111        }
112        
113        /**
114         * A constraint that ensures that each individual
115         * element of the scaling vector is within 
116         * +/- x standard deviations of the model. 
117         * 
118         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
119         *
120         */
121        public static class BoxConstraint implements Constraint {
122                double multiplier;
123                
124                /**
125                 * Construct with the given multiplier of the standard deviation.
126                 * @param multiplier
127                 */
128                public BoxConstraint(double multiplier) {
129                        this.multiplier = multiplier;
130                }
131                
132                @Override
133                public double[] apply(double[] in, double [] lamda) {
134                        double[] out = new double[in.length];
135                        
136                        for (int i=0; i<in.length; i++) {
137                                double w = multiplier * Math.sqrt(lamda[i]);
138                                out[i] = in[i] > w ? w : in[i] < -w ? -w : in[i];
139                        }
140                        
141                        return out;
142                }
143        }
144        
145        /**
146         * Constrain the scaling vector to a hyper-ellipsoid.
147         * 
148         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
149         *
150         */
151        public static class EllipsoidConstraint implements Constraint {
152                double dmax;
153                
154                /**
155                 * Construct with the given maximum normalised ellipsoid
156                 * radius. 
157                 * @param dmax
158                 */
159                public EllipsoidConstraint(double dmax) {
160                        this.dmax = dmax;
161                }
162                
163                @Override
164                public double[] apply(double[] in, double [] lamda) {
165                        double dmsq = 0;
166                        for (int i=0; i<in.length; i++) {
167                                dmsq += in[i] * in[i] / lamda[i];
168                        }
169                        
170                        if (dmsq < dmax*dmax) {
171                                return in;
172                        }
173                        
174                        double sc = dmax / Math.sqrt(dmsq);
175                        double[] out = new double[in.length];
176                        for (int i=0; i<in.length; i++) {
177                                out[i] = in[i] * sc;
178                        }
179                        
180                        return out;
181                }
182        }
183        
184        protected Constraint constraint;
185        protected PrincipalComponentAnalysis pc;
186        protected PointList mean;
187        protected int numComponents;
188        protected int maxIter = 100;
189
190        /**
191         * Construct a {@link PointDistributionModel} from the given data
192         * with a {@link NullConstraint}.
193         * 
194         * @param data
195         */
196        public PointDistributionModel(List<PointList> data) {
197                this(new NullConstraint(), data);
198        }
199        
200        /**
201         * Construct a {@link PointDistributionModel} from the given data
202         * and {@link Constraint}.
203         * 
204         * @param constraint 
205         * @param data
206         */
207        public PointDistributionModel(Constraint constraint, List<PointList> data) {
208                this.constraint = constraint;
209                
210                //align
211                mean = GeneralisedProcrustesAnalysis.alignPoints(data, 5, 10);
212                
213                //build data matrix
214                Matrix m = buildDataMatrix(data);
215                
216                //perform pca
217                this.pc = new SvdPrincipalComponentAnalysis();
218                pc.learnBasis(m);
219                
220                numComponents = this.pc.getEigenValues().length;
221        }
222        
223        private Matrix buildDataMatrix(PointList data) {
224                List<PointList> pls = new ArrayList<PointList>(1);
225                pls.add(data);
226                return buildDataMatrix(pls);
227        }
228        
229        private Matrix buildDataMatrix(List<PointList> data) {
230                final int nData = data.size();
231                final int nPoints = data.get(0).size();
232                
233                Matrix m = new Matrix(nData, nPoints * 2);
234                double[][] mData = m.getArray();
235                
236                for (int i=0; i<nData; i++) {
237                        PointList pts = data.get(i);
238                        for (int j=0, k=0; k<nPoints; j+=2, k++) {
239                                Point2d pt = pts.points.get(k);
240                                
241                                mData[i][j] = pt.getX();
242                                mData[i][j+1] = pt.getY();
243                        }
244                }
245                
246                return m;
247        }
248
249        /**
250         * @return the mean shape
251         */
252        public PointList getMean() {
253                return mean;
254        }
255        
256        /**
257         * Set the number of components of the PDM
258         * @param n number of components
259         */
260        public void setNumComponents(int n) {
261                pc.selectSubset(n);
262                numComponents = this.pc.getEigenValues().length;
263        }
264        
265        /**
266         * Set the number of components of the PDM using a {@link ComponentSelector}.
267         * @param selector the {@link ComponentSelector} to apply.
268         */
269        public void setNumComponents(ComponentSelector selector) {
270                pc.selectSubset(selector);
271                numComponents = this.pc.getEigenValues().length;
272        }
273        
274        /**
275         * Generate a plausible new shape from the scaling vector.
276         * The scaling vector is constrained by the underlying {@link Constraint}
277         * before being used to generate the model.
278         * @param scaling scaling vector.
279         * @return a new shape
280         */
281        public PointList generateNewShape(double [] scaling) {
282                PointList newShape = new PointList();
283                
284                double[] pts = pc.generate(constraint.apply(scaling, pc.getEigenValues()));
285                
286                for (int i=0; i<pts.length; i+=2) {
287                        float x = (float) pts[i];
288                        float y = (float) pts[i+1];
289                        
290                        newShape.points.add(new Point2dImpl(x, y));
291                }
292                
293                return newShape;
294        }
295        
296        /**
297         * Compute the standard deviations of the shape components, multiplied by the
298         * given value.
299         * @param multiplier the multiplier
300         * @return the multiplied standard deviations 
301         */
302        public double [] getStandardDeviations(double multiplier) {
303                double[] rngs = pc.getStandardDeviations();
304                
305                for (int i = 0; i < rngs.length; i++) {
306                        rngs[i] = rngs[i] * multiplier;
307                }
308                
309                return rngs;
310        }
311        
312        /**
313         * Determine the best parameters of the PDM for the given model.
314         * @param observed the observed model.
315         * @return the parameters that best fit the model.
316         */
317        public IndependentPair<Matrix, double []> fitModel(PointList observed) {
318                double [] model = new double[numComponents];
319                double delta = 1.0;
320                Matrix pose = null;
321                
322                ProcrustesAnalysis pa = new ProcrustesAnalysis(observed);
323                int count = 0;
324                while (delta > 1e-6 && count++ < maxIter) {
325                        PointList instance = this.generateNewShape(model);
326                        
327                        pose = pa.align(instance);
328                        
329                        PointList projected = observed.transform(pose.inverse());
330                        
331                        //TODO: tangent space projection???
332                        
333                        Matrix y = buildDataMatrix(projected);
334                        Matrix xbar = new Matrix(new double[][] { pc.getMean() });
335                        double[] newModel = (y.minus(xbar)).times(pc.getBasis()).getArray()[0];
336                        
337                        newModel = constraint.apply(newModel, pc.getEigenValues());
338                        
339                        delta = 0;
340                        for (int i=0; i<newModel.length; i++)
341                                delta += (newModel[i] - model[i])*(newModel[i] - model[i]);
342                        delta = Math.sqrt(delta);
343                        
344                        model = newModel;
345                }
346                
347                return new IndependentPair<Matrix, double[]>(pose, model);
348        }
349}