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.ar.epipolar.estimators;
17
18 import com.irurueta.algebra.AlgebraException;
19 import com.irurueta.algebra.Matrix;
20 import com.irurueta.algebra.SingularValueDecomposer;
21 import com.irurueta.algebra.Utils;
22 import com.irurueta.ar.epipolar.FundamentalMatrix;
23 import com.irurueta.ar.epipolar.InvalidFundamentalMatrixException;
24 import com.irurueta.geometry.Point2D;
25 import com.irurueta.geometry.ProjectiveTransformation2D;
26 import com.irurueta.geometry.estimators.LockedException;
27 import com.irurueta.geometry.estimators.NormalizerException;
28 import com.irurueta.geometry.estimators.NotReadyException;
29 import com.irurueta.geometry.estimators.Point2DNormalizer;
30
31 import java.util.List;
32
33 /**
34 * Non-robust fundamental matrix estimator for Affine camera projection model.
35 * This implementation uses 4 matched 2D points on left and right views.
36 */
37 public class AffineFundamentalMatrixEstimator extends FundamentalMatrixEstimator {
38
39 /**
40 * Constant indicating that by default an LMSE solution is not allowed.
41 */
42 public static final boolean DEFAULT_ALLOW_LMSE_SOLUTION = false;
43
44 /**
45 * Minimum number of matched 2D points to start the estimation.
46 */
47 public static final int MIN_REQUIRED_POINTS = 4;
48
49 /**
50 * Indicates if by default provided point correspondences are normalized to
51 * increase the accuracy of the estimation.
52 */
53 public static final boolean DEFAULT_NORMALIZE_POINT_CORRESPONDENCES = true;
54
55 /**
56 * Indicates whether an LMSE (the Least Mean Square Error) solution is allowed
57 * or not.
58 */
59 private boolean allowLMSESolution;
60
61 /**
62 * Indicates whether provided matched 2D points must be normalized to
63 * increase the accuracy of the estimation.
64 */
65 private boolean normalizePoints;
66
67 /**
68 * Constructor.
69 */
70 public AffineFundamentalMatrixEstimator() {
71 super();
72 allowLMSESolution = DEFAULT_ALLOW_LMSE_SOLUTION;
73 normalizePoints = DEFAULT_NORMALIZE_POINT_CORRESPONDENCES;
74 }
75
76 /**
77 * Constructor with matched 2D points.
78 *
79 * @param leftPoints 2D points on left view.
80 * @param rightPoints 2D points on right view.
81 * @throws IllegalArgumentException if provided list of points do not
82 * have the same length.
83 */
84 public AffineFundamentalMatrixEstimator(final List<Point2D> leftPoints, final List<Point2D> rightPoints) {
85 super(leftPoints, rightPoints);
86 allowLMSESolution = DEFAULT_ALLOW_LMSE_SOLUTION;
87 normalizePoints = DEFAULT_NORMALIZE_POINT_CORRESPONDENCES;
88 }
89
90 /**
91 * Returns boolean indicating whether an LMSE (the Least Mean Square Error)
92 * solution is allowed or not. When an LMSE solution is allowed, more than 8
93 * matched points can be used for fundamental matrix estimation. If LMSE
94 * solution is not allowed then only the 4 former matched points will be
95 * taken into account.
96 *
97 * @return true if an LMSE solution is allowed, false otherwise.
98 */
99 public boolean isLMSESolutionAllowed() {
100 return allowLMSESolution;
101 }
102
103 /**
104 * Sets boolean indicating whether an LMSE (the Least Mean Square Error)
105 * solution is allowed or not. When an LMSE solution is allowed, more than 8
106 * matched points can be used for fundamental matrix estimation. If LMSE
107 * solution is not allowed then only the 4 former matched points will be
108 * taken into account.
109 *
110 * @param allowed true if an LMSE solution is allowed, false otherwise.
111 * @throws LockedException if this instance is locked because an estimation
112 * is in progress.
113 */
114 public void setLMSESolutionAllowed(final boolean allowed) throws LockedException {
115 if (isLocked()) {
116 throw new LockedException();
117 }
118
119 allowLMSESolution = allowed;
120 }
121
122 /**
123 * Indicates whether provided matched 2D points must be normalized to
124 * increase the accuracy of the estimation.
125 *
126 * @return true if points must be normalized, false otherwise.
127 */
128 public boolean arePointsNormalized() {
129 return normalizePoints;
130 }
131
132 /**
133 * Sets boolean indicating whether provided matched 2D points must be
134 * normalized to increase the accuracy of the estimation.
135 *
136 * @param normalizePoints true if points must be normalized, false
137 * otherwise.
138 * @throws LockedException if this instance is locked because an estimation
139 * is in progress.
140 */
141 public void setPointsNormalized(final boolean normalizePoints) throws LockedException {
142 if (isLocked()) {
143 throw new LockedException();
144 }
145
146 this.normalizePoints = normalizePoints;
147 }
148
149 /**
150 * Returns boolean indicating whether estimator is ready to start the
151 * fundamental matrix estimation.
152 * This is true when the required minimum number of matched points is
153 * provided to obtain a solution and both left and right views have the
154 * same number of matched points.
155 *
156 * @return true if estimator is ready to start the fundamental matrix
157 * estimation, false otherwise.
158 */
159 @Override
160 public boolean isReady() {
161 return leftPoints != null && rightPoints != null && leftPoints.size() == rightPoints.size()
162 && leftPoints.size() >= MIN_REQUIRED_POINTS;
163 }
164
165 /**
166 * Estimates a fundamental matrix using provided lists of matched points on
167 * left and right views.
168 *
169 * @return a fundamental matrix.
170 * @throws LockedException if estimator is locked doing an estimation.
171 * @throws NotReadyException if estimator is not ready because required
172 * input points have not already been provided.
173 * @throws FundamentalMatrixEstimatorException if configuration of provided
174 * 2D points is degenerate and fundamental matrix
175 * estimation fails.
176 */
177 @SuppressWarnings("DuplicatedCode")
178 @Override
179 public FundamentalMatrix estimate() throws LockedException, NotReadyException, FundamentalMatrixEstimatorException {
180 if (isLocked()) {
181 throw new LockedException();
182 }
183 if (!isReady()) {
184 throw new NotReadyException();
185 }
186
187 locked = true;
188
189 if (listener != null) {
190 listener.onEstimateStart(this);
191 }
192
193 final var nPoints = leftPoints.size();
194
195 try {
196 ProjectiveTransformation2D leftNormalization = null;
197 ProjectiveTransformation2D rightNormalization = null;
198 final List<Point2D> leftPoints;
199 final List<Point2D> rightPoints;
200 if (normalizePoints) {
201 // normalize points on left view
202 final var normalizer = new Point2DNormalizer(this.leftPoints);
203 normalizer.compute();
204
205 leftNormalization = normalizer.getTransformation();
206
207 // normalize points on right view
208 normalizer.setPoints(this.rightPoints);
209 normalizer.compute();
210
211 rightNormalization = normalizer.getTransformation();
212
213 // normalize to increase accuracy
214 leftNormalization.normalize();
215 rightNormalization.normalize();
216
217 leftPoints = leftNormalization.transformPointsAndReturnNew(this.leftPoints);
218 rightPoints = rightNormalization.transformPointsAndReturnNew(this.rightPoints);
219 } else {
220 leftPoints = this.leftPoints;
221 rightPoints = this.rightPoints;
222 }
223
224 final Matrix a;
225 if (isLMSESolutionAllowed()) {
226 a = new Matrix(nPoints, 5);
227 } else {
228 a = new Matrix(MIN_REQUIRED_POINTS, 5);
229 }
230
231 Point2D leftPoint;
232 Point2D rightPoint;
233 double homLeftX;
234 double homLeftY;
235 double homLeftW;
236 double homRightX;
237 double homRightY;
238 double homRightW;
239 double value0;
240 double value1;
241 double value2;
242 double value3;
243 double value4;
244 double rowNorm;
245 for (var i = 0; i < nPoints; i++) {
246 leftPoint = leftPoints.get(i);
247 rightPoint = rightPoints.get(i);
248
249 // normalize points to increase accuracy
250 leftPoint.normalize();
251 rightPoint.normalize();
252
253 homLeftX = leftPoint.getHomX();
254 homLeftY = leftPoint.getHomY();
255 homLeftW = leftPoint.getHomW();
256
257 homRightX = rightPoint.getHomX();
258 homRightY = rightPoint.getHomY();
259 homRightW = rightPoint.getHomW();
260
261 // set a row values
262 value0 = homLeftW * homRightX;
263 value1 = homLeftW * homRightY;
264 value2 = homLeftX * homRightW;
265 value3 = homLeftY * homRightW;
266 value4 = homLeftW * homRightW;
267
268 // normalize row to increase accuracy
269 rowNorm = Math.sqrt(Math.pow(value0, 2.0)
270 + Math.pow(value1, 2.0) + Math.pow(value2, 2.0)
271 + Math.pow(value3, 2.0) + Math.pow(value4, 2.0));
272
273 a.setElementAt(i, 0, value0 / rowNorm);
274 a.setElementAt(i, 1, value1 / rowNorm);
275 a.setElementAt(i, 2, value2 / rowNorm);
276 a.setElementAt(i, 3, value3 / rowNorm);
277 a.setElementAt(i, 4, value4 / rowNorm);
278
279 if (!isLMSESolutionAllowed() && i == (MIN_REQUIRED_POINTS - 1)) {
280 break;
281 }
282 }
283
284 final var decomposer = new SingularValueDecomposer(a);
285
286 decomposer.decompose();
287
288 // if nullity of provided a matrix is not of dimension 1 (number of
289 // dimensions of null-space), then epipolar geometry is degenerate
290 // because there is more than one possible solution (up to scale).
291 // This is typically due to co-linearities or co-planarities on
292 // projected 2D points. In this case we throw an exception
293 if (decomposer.getNullity() > 1) {
294 throw new FundamentalMatrixEstimatorException();
295 }
296
297 var v = decomposer.getV();
298
299 // The fundamental matrix is contained in vector form on the last
300 // column of V, we reshape such vector into a 3x3 matrix
301 var fundMatrix = new Matrix(FundamentalMatrix.FUNDAMENTAL_MATRIX_ROWS,
302 FundamentalMatrix.FUNDAMENTAL_MATRIX_COLS);
303 fundMatrix.setElementAt(0, 2, v.getElementAt(0, 4));
304 fundMatrix.setElementAt(1, 2, v.getElementAt(1, 4));
305 fundMatrix.setElementAt(2, 0, v.getElementAt(2, 4));
306 fundMatrix.setElementAt(2, 1, v.getElementAt(3, 4));
307 fundMatrix.setElementAt(2, 2, v.getElementAt(4, 4));
308
309 if (normalizePoints && leftNormalization != null) {
310 // denormalize fundMatrix
311 final var transposedRightTransformationMatrix = rightNormalization.asMatrix().transposeAndReturnNew();
312 final var leftTransformationMatrix = leftNormalization.asMatrix();
313
314 // compute fundMatrix = transposedRightTransformationMatrix *
315 // fundMatrix * leftTransformationMatrix
316 fundMatrix.multiply(leftTransformationMatrix);
317 transposedRightTransformationMatrix.multiply(fundMatrix);
318 fundMatrix = transposedRightTransformationMatrix;
319
320 // normalize by Frobenius norm to increase accuracy after point
321 // de-normalization
322 final var norm = Utils.normF(fundMatrix);
323 fundMatrix.multiplyByScalar(1.0 / norm);
324 }
325
326 // enforce rank 2
327 decomposer.setInputMatrix(fundMatrix);
328
329 decomposer.decompose();
330
331 // if rank is not already correct, then we enforce it
332 final var rank = decomposer.getRank();
333 if (rank > FundamentalMatrix.FUNDAMENTAL_MATRIX_RANK) {
334 // rank needs to be reduced
335 final var u = decomposer.getU();
336 final var w = decomposer.getW();
337 v = decomposer.getV();
338
339 // transpose V
340 v.transpose();
341 final var transV = v;
342
343 // set last singular value to zero to enforce rank 2
344 w.setElementAt(2, 2, 0.0);
345
346 // compute fundMatrix = U * W * V'
347 w.multiply(transV);
348 u.multiply(w);
349 fundMatrix = u;
350 } else if (rank < FundamentalMatrix.FUNDAMENTAL_MATRIX_RANK) {
351 // rank is 1, which is lower than required fundamental matrix
352 // rank (rank 2)
353 throw new FundamentalMatrixEstimatorException();
354 }
355
356 final var result = new FundamentalMatrix(fundMatrix);
357
358 if (listener != null) {
359 listener.onEstimateEnd(this, result);
360 }
361
362 return result;
363
364 } catch (final InvalidFundamentalMatrixException | AlgebraException | NormalizerException e) {
365 throw new FundamentalMatrixEstimatorException(e);
366 } finally {
367 locked = false;
368 }
369 }
370
371 /**
372 * Returns method of non-robust fundamental matrix estimator.
373 *
374 * @return method of fundamental matrix estimator.
375 */
376 @Override
377 public FundamentalMatrixEstimatorMethod getMethod() {
378 return FundamentalMatrixEstimatorMethod.AFFINE_ALGORITHM;
379 }
380
381 /**
382 * Returns minimum number of matched pair of points required to start
383 * the estimation. This implementation requires a minimum of 4 points
384 *
385 * @return minimum number of matched pair of points required to start
386 * the estimation. Always returns 4.
387 */
388 @Override
389 public int getMinRequiredPoints() {
390 return MIN_REQUIRED_POINTS;
391 }
392 }