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}