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 }