1 /* 2 * This file is part of ELKI: 3 * Environment for Developing KDD-Applications Supported by Index-Structures 4 * 5 * Copyright (C) 2018 6 * ELKI Development Team 7 * 8 * This program is free software: you can redistribute it and/or modify 9 * it under the terms of the GNU Affero General Public License as published by 10 * the Free Software Foundation, either version 3 of the License, or 11 * (at your option) any later version. 12 * 13 * This program is distributed in the hope that it will be useful, 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 * GNU Affero General Public License for more details. 17 * 18 * You should have received a copy of the GNU Affero General Public License 19 * along with this program. If not, see <http://www.gnu.org/licenses/>. 20 */ 21 package de.lmu.ifi.dbs.elki.math.linearalgebra; 22 23 import static de.lmu.ifi.dbs.elki.math.linearalgebra.VMath.inverse; 24 import static de.lmu.ifi.dbs.elki.math.linearalgebra.VMath.times; 25 import static de.lmu.ifi.dbs.elki.math.linearalgebra.VMath.unitMatrix; 26 import static de.lmu.ifi.dbs.elki.math.linearalgebra.VMath.zeroMatrix; 27 28 import java.util.Arrays; 29 30 import net.jafama.DoubleWrapper; 31 import net.jafama.FastMath; 32 33 /** 34 * Affine transformations implemented using homogeneous coordinates. 35 * 36 * The use of homogeneous coordinates allows the combination of multiple affine 37 * transformations (rotations, translations, scaling) into a single matrix 38 * operation (of dimensionality dim+1), and also the construction of an inverse 39 * transformation. 40 * 41 * @author Erich Schubert 42 * @since 0.2 43 * 44 * @composed - - - Matrix 45 * @assoc - - - Matrix 46 */ 47 public class AffineTransformation { 48 /** 49 * the dimensionality of the transformation 50 */ 51 private final int dim; 52 53 /** 54 * The transformation matrix of dim+1 x dim+1 for homogeneous coordinates 55 */ 56 private double[][] trans; 57 58 /** 59 * the inverse transformation 60 */ 61 private double[][] inv; 62 63 /** 64 * Constructor for an identity transformation. 65 * 66 * @param dim dimensionality 67 */ AffineTransformation(int dim)68 public AffineTransformation(int dim) { 69 super(); 70 this.dim = dim; 71 this.trans = unitMatrix(dim + 1); 72 } 73 74 /** 75 * Trivial constructor with all fields, mostly for cloning 76 * 77 * @param dim dimensionality 78 * @param trans transformation matrix (will NOT be copied!) 79 * @param inv inverse matrix (will NOT be copied!) 80 */ AffineTransformation(int dim, double[][] trans, double[][] inv)81 public AffineTransformation(int dim, double[][] trans, double[][] inv) { 82 super(); 83 this.dim = dim; 84 this.trans = trans; 85 this.inv = inv; 86 } 87 88 /** 89 * Generate a transformation that reorders axes in the given way. 90 * 91 * The list of axes to be used should not contain duplicates, or the resulting 92 * matrix will not be invertible. It does not have to be complete however, in 93 * particular an empty list will result in the identity transform: unmentioned 94 * axes will be appended in their original order. 95 * 96 * @param dim Dimensionality of vector space (resulting Matrix will be dim+1 x 97 * dim+1) 98 * @param axes (Partial) list of axes 99 * @return new transformation to do the requested reordering 100 */ reorderAxesTransformation(int dim, int... axes)101 public static AffineTransformation reorderAxesTransformation(int dim, int... axes) { 102 double[][] m = zeroMatrix(dim + 1); 103 // insert ones appropriately: 104 for(int i = 0; i < axes.length; i++) { 105 assert (0 < axes[i] && axes[i] <= dim); 106 m[i][axes[i] - 1] = 1.0; 107 } 108 int useddim = 1; 109 for(int i = axes.length; i < dim + 1; i++) { 110 // find next "unused" dimension. 111 { 112 boolean search = true; 113 while(search) { 114 search = false; 115 for(int a : axes) { 116 if(a == useddim) { 117 search = true; 118 useddim++; 119 break; 120 } 121 } 122 } 123 } 124 m[i][useddim - 1] = 1.0; 125 useddim++; 126 } 127 assert (useddim - 2 == dim); 128 return new AffineTransformation(dim, m, null); 129 } 130 131 /** 132 * Return a clone of the affine transformation 133 * 134 * @return cloned affine transformation 135 */ 136 @Override clone()137 public AffineTransformation clone() { 138 // Note that we're NOT using copied matrices here, since this class 139 // supposedly never modifies it's matrixes but replaces them with new 140 // ones. Thus it is safe to re-use it for a cloned copy. 141 return new AffineTransformation(this.dim, this.trans, this.inv); 142 } 143 144 /** 145 * Query dimensionality of the transformation. 146 * 147 * @return dimensionality 148 */ getDimensionality()149 public int getDimensionality() { 150 return dim; 151 } 152 153 /** 154 * Add a translation operation to the matrix 155 * 156 * @param v translation vector 157 */ addTranslation(double[] v)158 public void addTranslation(double[] v) { 159 assert (v.length == dim); 160 161 // reset inverse transformation - needs recomputation. 162 inv = null; 163 164 double[][] homTrans = unitMatrix(dim + 1); 165 for(int i = 0; i < dim; i++) { 166 homTrans[i][dim] = v[i]; 167 } 168 trans = times(homTrans, trans); 169 } 170 171 /** 172 * Add a matrix operation to the matrix. 173 * 174 * Be careful to use only invertible matrices if you want an invertible affine 175 * transformation. 176 * 177 * @param m matrix (should be invertible) 178 */ addMatrix(double[][] m)179 public void addMatrix(double[][] m) { 180 assert (m.length == dim); 181 assert (m[0].length == dim); 182 183 // reset inverse transformation - needs recomputation. 184 inv = null; 185 186 // extend the matrix with an extra row and column 187 double[][] ht = new double[dim + 1][dim + 1]; 188 for(int i = 0; i < dim; i++) { 189 for(int j = 0; j < dim; j++) { 190 ht[i][j] = m[i][j]; 191 } 192 } 193 // the other cells default to identity matrix 194 ht[dim][dim] = 1.0; 195 // Multiply from left. 196 trans = times(ht, trans); 197 } 198 199 /** 200 * Convenience function to apply a rotation in 2 dimensions. 201 * 202 * @param axis1 first dimension 203 * @param axis2 second dimension 204 * @param angle rotation angle in radians. 205 */ addRotation(int axis1, int axis2, double angle)206 public void addRotation(int axis1, int axis2, double angle) { 207 // TODO: throw an exception instead of using assert 208 assert (axis1 >= 0); 209 assert (axis1 < dim); 210 assert (axis1 >= 0); 211 assert (axis2 < dim); 212 assert (axis1 != axis2); 213 214 // reset inverse transformation - needs recomputation. 215 inv = null; 216 217 double[][] ht = new double[dim + 1][dim + 1]; 218 // identity matrix 219 for(int i = 0; i < dim + 1; i++) { 220 ht[i][i] = 1.0; 221 } 222 // insert rotation values 223 final DoubleWrapper tmp = new DoubleWrapper(); // To return cosine 224 double s = FastMath.sinAndCos(angle, tmp), c = tmp.value; 225 ht[axis1][axis1] = +c; 226 ht[axis1][axis2] = -s; 227 ht[axis2][axis1] = +s; 228 ht[axis2][axis2] = +c; 229 // Multiply from left 230 trans = times(ht, trans); 231 } 232 233 /** 234 * Add a reflection along the given axis. 235 * 236 * @param axis Axis number to do the reflection at. 237 */ 238 public void addAxisReflection(int axis) { 239 assert (0 < axis && axis <= dim); 240 // reset inverse transformation - needs recomputation. 241 inv = null; 242 243 // Formal: 244 // Matrix homTrans = Matrix.unitMatrix(dim + 1); 245 // homTrans[axis - 1][axis - 1] = -1; 246 // trans = homTrans.times(trans); 247 // Faster: 248 for(int i = 0; i <= dim; i++) { 249 trans[axis - 1][i] = -trans[axis - 1][i]; 250 } 251 } 252 253 /** 254 * Simple linear (symmetric) scaling. 255 * 256 * @param scale Scaling factor 257 */ 258 public void addScaling(double scale) { 259 // invalidate inverse 260 inv = null; 261 // Note: last ROW is not included. 262 for(int i = 0; i < dim; i++) { 263 for(int j = 0; j <= dim; j++) { 264 trans[i][j] = trans[i][j] * scale; 265 } 266 } 267 // As long as relative vectors aren't used, this would also work: 268 // trans[dim][dim] = trans[dim][dim] / scale; 269 } 270 271 /** 272 * Get the transformation matrix 273 * 274 * @return the transformation matrix 275 */ 276 public double[][] getTransformation() { 277 return trans; 278 } 279 280 /** 281 * Get a the inverse matrix 282 * 283 * @return the inverse transformation matrix 284 */ 285 public double[][] getInverse() { 286 if(inv == null) { 287 updateInverse(); 288 } 289 return inv; 290 } 291 292 /** 293 * Compute the inverse transformation matrix 294 */ 295 private void updateInverse() { 296 inv = inverse(trans); 297 } 298 299 /** 300 * Transform an absolute vector into homogeneous coordinates. 301 * 302 * @param v initial vector 303 * @return vector of dim+1, with new column having the value 1.0 304 */ 305 public double[] homogeneVector(double[] v) { 306 assert (v.length == dim); 307 double[] dv = Arrays.copyOf(v, dim + 1); 308 dv[dim] = 1.0; 309 return dv; 310 } 311 312 /** 313 * Transform a relative vector into homogeneous coordinates. 314 * 315 * @param v initial vector 316 * @return vector of dim+1, with new column having the value 0.0 317 */ 318 public double[] homogeneRelativeVector(double[] v) { 319 assert (v.length == dim); 320 // TODO: this only works properly when trans[dim][dim] == 1.0, right? 321 double[] dv = Arrays.copyOf(v, dim + 1); 322 dv[dim] = 0.0; 323 return dv; 324 } 325 326 /** 327 * Project an homogeneous vector back into the original space. 328 * 329 * @param v Matrix of 1 x dim+1 containing the homogeneous vector 330 * @return vector of dimension dim 331 */ 332 public double[] unhomogeneVector(double[] v) { 333 assert (v.length == dim + 1); 334 // TODO: this only works properly when trans[dim][dim] == 1.0, right? 335 double[] dv = new double[dim]; 336 double scale = v[dim]; 337 assert (Math.abs(scale) > 0.0); 338 for(int i = 0; i < dim; i++) { 339 dv[i] = v[i] / scale; 340 } 341 return dv; 342 } 343 344 /** 345 * Project an homogeneous vector back into the original space. 346 * 347 * @param v Matrix of 1 x dim+1 containing the homogeneous vector 348 * @return vector of dimension dim 349 */ 350 public double[] unhomogeneRelativeVector(double[] v) { 351 assert (v.length == dim + 1); 352 double[] dv = new double[dim]; 353 System.arraycopy(v, 0, dv, 0, dim); 354 assert (Math.abs(v[dim]) < Double.MIN_NORMAL); 355 return dv; 356 } 357 358 /** 359 * Apply the transformation onto a vector 360 * 361 * @param v vector of dimensionality dim 362 * @return transformed vector of dimensionality dim 363 */ 364 public double[] apply(double[] v) { 365 return unhomogeneVector(times(trans, homogeneVector(v))); 366 } 367 368 /** 369 * Apply the inverse transformation onto a vector 370 * 371 * @param v vector of dimensionality dim 372 * @return transformed vector of dimensionality dim 373 */ 374 public double[] applyInverse(double[] v) { 375 if(inv == null) { 376 updateInverse(); 377 } 378 return unhomogeneVector(times(inv, homogeneVector(v))); 379 } 380 381 /** 382 * Apply the transformation onto a vector 383 * 384 * @param v vector of dimensionality dim 385 * @return transformed vector of dimensionality dim 386 */ 387 public double[] applyRelative(double[] v) { 388 return unhomogeneRelativeVector(times(trans, homogeneRelativeVector(v))); 389 } 390 391 /** 392 * Apply the inverse transformation onto a vector 393 * 394 * @param v vector of dimensionality dim 395 * @return transformed vector of dimensionality dim 396 */ 397 public double[] applyRelativeInverse(double[] v) { 398 if(inv == null) { 399 updateInverse(); 400 } 401 return unhomogeneRelativeVector(times(inv, homogeneRelativeVector(v))); 402 } 403 }