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}