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.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 }