View Javadoc
1   /*
2    * Copyright (C) 2017 Alberto Irurueta Carro (alberto@irurueta.com)
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package com.irurueta.geometry.estimators;
17  
18  import com.irurueta.algebra.AlgebraException;
19  import com.irurueta.algebra.ArrayUtils;
20  import com.irurueta.algebra.Matrix;
21  import com.irurueta.algebra.SingularValueDecomposer;
22  import com.irurueta.algebra.Utils;
23  import com.irurueta.geometry.CoincidentPointsException;
24  import com.irurueta.geometry.InvalidRotationMatrixException;
25  import com.irurueta.geometry.MetricTransformation2D;
26  import com.irurueta.geometry.Point2D;
27  import com.irurueta.geometry.Rotation2D;
28  
29  import java.util.List;
30  
31  /**
32   * Estimator of a 2D metric transformation based on point correspondences.
33   * A minimum of 3 non-coincident matched 2D input/output points is required for
34   * estimation.
35   * If more points are provided an LMSE (Least Mean Squared Error) solution will
36   * be found.
37   */
38  @SuppressWarnings("DuplicatedCode")
39  public class MetricTransformation2DEstimator {
40  
41      /**
42       * Minimum required number of matched points.
43       */
44      public static final int MINIMUM_SIZE = 3;
45  
46      /**
47       * For some point configurations a solution can be found with only 2 points.
48       */
49      public static final int WEAK_MINIMUM_SIZE = 2;
50  
51  
52      /**
53       * 2D input points.
54       */
55      private List<Point2D> inputPoints;
56  
57      /**
58       * 2D output points.
59       */
60      private List<Point2D> outputPoints;
61  
62      /**
63       * Listener to be notified of events such as when estimation starts or ends.
64       */
65      private MetricTransformation2DEstimatorListener listener;
66  
67      /**
68       * Indicates whether estimation can start with only 2 points or not.
69       * True allows 2 points, false requires 3.
70       */
71      private boolean weakMinimumSizeAllowed;
72  
73      /**
74       * Indicates if this estimator is locked because an estimation is being
75       * computed.
76       */
77      private boolean locked;
78  
79      /**
80       * Constructor.
81       */
82      public MetricTransformation2DEstimator() {
83      }
84  
85      /**
86       * Constructor.
87       *
88       * @param inputPoints  2D input points.
89       * @param outputPoints 2D output points.
90       * @throws IllegalArgumentException if provided lists of points don't have
91       *                                  the same size or their size is smaller than 2.
92       */
93      public MetricTransformation2DEstimator(final List<Point2D> inputPoints, final List<Point2D> outputPoints) {
94          internalSetPoints(inputPoints, outputPoints);
95      }
96  
97      /**
98       * Constructor.
99       *
100      * @param listener listener to be notified of events such as when estimation
101      *                 starts or ends.
102      */
103     public MetricTransformation2DEstimator(final MetricTransformation2DEstimatorListener listener) {
104         this.listener = listener;
105     }
106 
107     /**
108      * Constructor.
109      *
110      * @param listener     listener to be notified of events such as when estimation
111      *                     starts or ends.
112      * @param inputPoints  3D input points.
113      * @param outputPoints 3D output points.
114      * @throws IllegalArgumentException if provided lists of points don't have
115      *                                  the same size or their size is smaller than 2.
116      */
117     public MetricTransformation2DEstimator(
118             final MetricTransformation2DEstimatorListener listener,
119             final List<Point2D> inputPoints, final List<Point2D> outputPoints) {
120         this.listener = listener;
121         internalSetPoints(inputPoints, outputPoints);
122     }
123 
124     /**
125      * Constructor.
126      *
127      * @param weakMinimumSizeAllowed true allows 3 points, false requires 4.
128      */
129     public MetricTransformation2DEstimator(final boolean weakMinimumSizeAllowed) {
130         this.weakMinimumSizeAllowed = weakMinimumSizeAllowed;
131     }
132 
133     /**
134      * Constructor.
135      *
136      * @param inputPoints            2D input points.
137      * @param outputPoints           2D output points.
138      * @param weakMinimumSizeAllowed true allows 3 points, false requires 4.
139      * @throws IllegalArgumentException if provided lists of points don't have
140      *                                  the same size or their size is smaller than 2.
141      */
142     public MetricTransformation2DEstimator(
143             final List<Point2D> inputPoints, final List<Point2D> outputPoints, final boolean weakMinimumSizeAllowed) {
144         this.weakMinimumSizeAllowed = weakMinimumSizeAllowed;
145         internalSetPoints(inputPoints, outputPoints);
146     }
147 
148     /**
149      * Constructor.
150      *
151      * @param listener               listener to be notified of events such as when estimation
152      *                               starts or ends.
153      * @param weakMinimumSizeAllowed true allows 3 points, false requires 4.
154      */
155     public MetricTransformation2DEstimator(
156             final MetricTransformation2DEstimatorListener listener, final boolean weakMinimumSizeAllowed) {
157         this.weakMinimumSizeAllowed = weakMinimumSizeAllowed;
158         this.listener = listener;
159     }
160 
161     /**
162      * Constructor.
163      *
164      * @param listener               listener to be notified of events such as when estimation
165      *                               starts or ends.
166      * @param inputPoints            3D input points.
167      * @param outputPoints           3D output points.
168      * @param weakMinimumSizeAllowed true allows 3 points, false requires 4.
169      * @throws IllegalArgumentException if provided lists of points don't have
170      *                                  the same size or their size is smaller than 2.
171      */
172     public MetricTransformation2DEstimator(
173             final MetricTransformation2DEstimatorListener listener,
174             final List<Point2D> inputPoints, final List<Point2D> outputPoints, final boolean weakMinimumSizeAllowed) {
175         this.weakMinimumSizeAllowed = weakMinimumSizeAllowed;
176         this.listener = listener;
177         internalSetPoints(inputPoints, outputPoints);
178     }
179 
180     /**
181      * Returns list of input points to be used to estimate a metric 2D
182      * transformation.
183      * Each point in the list of input points must be matched with the
184      * corresponding point in the list of output points located at the same
185      * position. Hence, both input points and output points must have the same
186      * size, and their size must be greater or equal than MINIMUM_SIZE.
187      *
188      * @return list of input points to be used to estimate a metric 2D
189      * transformation.
190      */
191     public List<Point2D> getInputPoints() {
192         return inputPoints;
193     }
194 
195     /**
196      * Returns list of output points to be used to estimate a metric 2D
197      * transformation.
198      * Each point in the list of output points must be matched with the
199      * corresponding point in the list of input points located at the same
200      * position. Hence, both input points and output points must have the same
201      * size, and their size must be greater or equal than MINIMUM_SIZE.
202      *
203      * @return list of output points to be used to estimate a metric 2D
204      * transformation.
205      */
206     public List<Point2D> getOutputPoints() {
207         return outputPoints;
208     }
209 
210     /**
211      * Sets list of points to be used to estimate a metric 2D transformation.
212      * Points in the list located at the same position are considered to be
213      * matched. Hence, both lists must have the same size, and their size must
214      * be greater or equal than MINIMUM_SIZE.
215      *
216      * @param inputPoints  list of input points to be used to estimate a metric
217      *                     2D transformation.
218      * @param outputPoints list of output points to be used to estimate a metric
219      *                     2D transformation.
220      * @throws IllegalArgumentException if provided lists of points don't have
221      *                                  the same size or their size is smaller than MINIMUM_SIZE.
222      * @throws LockedException          if estimator is locked because a computation is
223      *                                  already in progress.
224      */
225     public void setPoints(final List<Point2D> inputPoints, final List<Point2D> outputPoints) throws LockedException {
226         if (isLocked()) {
227             throw new LockedException();
228         }
229         internalSetPoints(inputPoints, outputPoints);
230     }
231 
232     /**
233      * Returns reference to listener to be notified of events such as when
234      * estimation starts or ends.
235      *
236      * @return listener to be notified of events.
237      */
238     public MetricTransformation2DEstimatorListener getListener() {
239         return listener;
240     }
241 
242     /**
243      * Sets listener to be notified of events such as when estimation starts or
244      * ends.
245      *
246      * @param listener listener to be notified of events.
247      * @throws LockedException if estimator is locked.
248      */
249     public void setListener(final MetricTransformation2DEstimatorListener listener) throws LockedException {
250         if (isLocked()) {
251             throw new LockedException();
252         }
253         this.listener = listener;
254     }
255 
256     /**
257      * Indicates whether estimation can start with only 2 points or not.
258      *
259      * @return true allows 2 points, false requires 3.
260      */
261     public boolean isWeakMinimumSizeAllowed() {
262         return weakMinimumSizeAllowed;
263     }
264 
265     /**
266      * Specifies whether estimation can start with only 2 points or not.
267      *
268      * @param weakMinimumSizeAllowed true allows 2 points, false requires 3.
269      * @throws LockedException if estimator is locked.
270      */
271     public void setWeakMinimumSizeAllowed(final boolean weakMinimumSizeAllowed) throws LockedException {
272         if (isLocked()) {
273             throw new LockedException();
274         }
275         this.weakMinimumSizeAllowed = weakMinimumSizeAllowed;
276     }
277 
278     /**
279      * Required minimum number of point correspondences to start the estimation.
280      * Can be either 2 or 3.
281      *
282      * @return minimum number of point correspondences.
283      */
284     public int getMinimumPoints() {
285         return weakMinimumSizeAllowed ? WEAK_MINIMUM_SIZE : MINIMUM_SIZE;
286     }
287 
288     /**
289      * Indicates whether listener has been provided and is available for
290      * retrieval.
291      *
292      * @return true if available, false otherwise.
293      */
294     public boolean isListenerAvailable() {
295         return listener != null;
296     }
297 
298     /**
299      * Indicates if this instance is locked because estimation is being
300      * computed.
301      *
302      * @return true if locked, false otherwise.
303      */
304     public boolean isLocked() {
305         return locked;
306     }
307 
308     /**
309      * Indicates if estimator is ready to start the metric 2D transformation
310      * estimation.
311      * This is true when input data (i.e. lists of matched points) are provided
312      * and a minimum of MINIMUM_SIZE points are available.
313      *
314      * @return true if estimator is ready, false otherwise.
315      */
316     public boolean isReady() {
317         return inputPoints != null && outputPoints != null && inputPoints.size() == outputPoints.size()
318                 && inputPoints.size() >= getMinimumPoints();
319     }
320 
321     /**
322      * Estimates a metric 2D transformation using the list of matched input
323      * and output 2D points.
324      * A minimum of 2 matched non-coincident points is required. If more points
325      * are provided an LMSE (Least Mean Squared Error) solution  will be found.
326      *
327      * @return estimated metric 2D transformation.
328      * @throws LockedException           if estimator is locked.
329      * @throws NotReadyException         if not enough data has been provided.
330      * @throws CoincidentPointsException raised if transformation cannot be
331      *                                   estimated for some reason (point configuration degeneracy, duplicate
332      *                                   points or numerical instabilities).
333      */
334     public MetricTransformation2D estimate() throws LockedException, NotReadyException, CoincidentPointsException {
335         final var result = new MetricTransformation2D();
336         estimate(result);
337         return result;
338     }
339 
340     /**
341      * Estimates a metric 2D transformation using the list of matched input and
342      * output 2D points.
343      * A minimum of 2 matched non-coincident points is required. If more points
344      * are provided an LMSE (Least Mean Squared Error) solution will be found.
345      *
346      * @param result instance where result will be stored.
347      * @throws LockedException           if estimator is locked.
348      * @throws NotReadyException         if not enough data has been provided.
349      * @throws CoincidentPointsException raised if transformation cannot be
350      *                                   estimated for some reason (point configuration degeneracy, duplicate
351      *                                   points or numerical instabilities).
352      */
353     public void estimate(final MetricTransformation2D result) throws LockedException, NotReadyException,
354             CoincidentPointsException {
355         if (isLocked()) {
356             throw new LockedException();
357         }
358         if (!isReady()) {
359             throw new NotReadyException();
360         }
361 
362         try {
363             locked = true;
364 
365             if (listener != null) {
366                 listener.onEstimateStart(this);
367             }
368 
369             final var inCentroid = computeCentroid(inputPoints);
370             final var outCentroid = computeCentroid(outputPoints);
371 
372             final var m = new Matrix(Point2D.POINT2D_INHOMOGENEOUS_COORDINATES_LENGTH,
373                     Point2D.POINT2D_INHOMOGENEOUS_COORDINATES_LENGTH);
374 
375             final var n = inputPoints.size();
376             Point2D inputPoint;
377             Point2D outputPoint;
378             final var col = new Matrix(Point2D.POINT2D_INHOMOGENEOUS_COORDINATES_LENGTH, 1);
379             final var row = new Matrix(1, Point2D.POINT2D_INHOMOGENEOUS_COORDINATES_LENGTH);
380             final var tmp = new Matrix(Point2D.POINT2D_INHOMOGENEOUS_COORDINATES_LENGTH,
381                     Point2D.POINT2D_INHOMOGENEOUS_COORDINATES_LENGTH);
382             var inCov = 0.0;
383             for (var i = 0; i < n; i++) {
384                 inputPoint = inputPoints.get(i);
385                 outputPoint = outputPoints.get(i);
386 
387                 col.setElementAtIndex(0, inputPoint.getInhomX() - inCentroid.getElementAtIndex(0));
388                 col.setElementAtIndex(1, inputPoint.getInhomY() - inCentroid.getElementAtIndex(1));
389 
390                 row.setElementAtIndex(0, outputPoint.getInhomX() - outCentroid.getElementAtIndex(0));
391                 row.setElementAtIndex(1, outputPoint.getInhomY() - outCentroid.getElementAtIndex(1));
392 
393                 // compute covariances of input and output points
394                 inCov += Math.pow(Utils.normF(col), 2.0);
395 
396                 col.multiply(row, tmp);
397                 m.add(tmp);
398             }
399 
400             if (inCov == 0.0) {
401                 throw new CoincidentPointsException();
402             }
403 
404             final var decomposer = new SingularValueDecomposer(m);
405             decomposer.decompose();
406 
407             if (!weakMinimumSizeAllowed && decomposer.getNullity() > 0) {
408                 throw new CoincidentPointsException();
409             }
410 
411             final var u = decomposer.getU();
412             final var v = decomposer.getV();
413 
414             final var s = decomposer.getSingularValues();
415 
416             // rotation R = V*U^T
417             final var r = v.multiplyAndReturnNew(u.transposeAndReturnNew());
418 
419             final var e = new double[]{1.0, 1.0};
420 
421             if (Utils.det(r) < 0.0) {
422                 // ideally rotation has 2 unitary singular values.
423                 // Because of reflection, we must change sign of last singular
424                 // value and reconstruct rotation matrix
425 
426                 e[1] = -1.0;
427 
428                 // because rotation matrix can be seen as V*e*U^T, we can
429                 // simply multiply 3rd column of R by -1
430                 r.setElementAt(0, 1, -r.getElementAt(0, 1));
431                 r.setElementAt(1, 1, -r.getElementAt(1, 1));
432             }
433 
434             final var rotation = new Rotation2D(r);
435 
436             // scale
437             final var dot = ArrayUtils.dotProduct(s, e);
438             final var invScale = dot / inCov;
439 
440             // translation
441             final var t = r.multiplyAndReturnNew(inCentroid);
442             t.multiplyByScalar(-invScale);
443             t.add(outCentroid);
444 
445             result.setRotation(rotation);
446             result.setTranslation(t.getBuffer());
447             result.setScale(invScale);
448 
449             if (listener != null) {
450                 listener.onEstimateEnd(this);
451             }
452         } catch (final AlgebraException | InvalidRotationMatrixException e) {
453             throw new CoincidentPointsException(e);
454         } finally {
455             locked = false;
456         }
457     }
458 
459     /**
460      * Computes centroid of provided list of points using inhomogeneous
461      * coordinates.
462      *
463      * @param points list of points to compute centroid.
464      * @return centroid.
465      * @throws AlgebraException never thrown.
466      */
467     private static Matrix computeCentroid(final List<Point2D> points) throws AlgebraException {
468         var x = 0.0;
469         var y = 0.0;
470         var n = points.size();
471         for (final var p : points) {
472             x += p.getInhomX() / n;
473             y += p.getInhomY() / n;
474         }
475 
476         final var result = new Matrix(Point2D.POINT2D_INHOMOGENEOUS_COORDINATES_LENGTH, 1);
477         result.setElementAtIndex(0, x);
478         result.setElementAtIndex(1, y);
479         return result;
480     }
481 
482     /**
483      * Internal method to set lists of points to be used to estimate a
484      * metric 2D transformation.
485      * This method does not check whether estimator is locked or not.
486      *
487      * @param inputPoints  list of input points to be used to estimate a
488      *                     metric 2D transformation.
489      * @param outputPoints list of output points to be used to estimate a
490      *                     metric 2D transformation.
491      * @throws IllegalArgumentException if provided lists of points don't have
492      *                                  the same size or their size is smaller than #getMinimumPoints.
493      */
494     private void internalSetPoints(final List<Point2D> inputPoints, final List<Point2D> outputPoints) {
495         if (inputPoints.size() < getMinimumPoints()) {
496             throw new IllegalArgumentException();
497         }
498         if (inputPoints.size() != outputPoints.size()) {
499             throw new IllegalArgumentException();
500         }
501         this.inputPoints = inputPoints;
502         this.outputPoints = outputPoints;
503     }
504 }