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 }