View Javadoc
1   /*
2    * Copyright (C) 2015 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.sfm;
17  
18  import com.irurueta.geometry.CoordinatesType;
19  import com.irurueta.geometry.PinholeCamera;
20  import com.irurueta.geometry.Point2D;
21  import com.irurueta.geometry.Point3D;
22  import com.irurueta.geometry.estimators.LockedException;
23  import com.irurueta.geometry.estimators.NotReadyException;
24  import com.irurueta.numerical.robust.LMedSRobustEstimator;
25  import com.irurueta.numerical.robust.LMedSRobustEstimatorListener;
26  import com.irurueta.numerical.robust.RobustEstimator;
27  import com.irurueta.numerical.robust.RobustEstimatorException;
28  import com.irurueta.numerical.robust.RobustEstimatorMethod;
29  
30  import java.util.ArrayList;
31  import java.util.List;
32  
33  /**
34   * Robustly triangulates 3D points from matched 2D points and their
35   * corresponding cameras on several views using LMedS algorithm.
36   */
37  public class LMedSRobustSinglePoint3DTriangulator extends RobustSinglePoint3DTriangulator {
38  
39      /**
40       * Default value to be used for stop threshold. Stop threshold can be used
41       * to keep the algorithm iterating in case that best estimated threshold
42       * using median of residuals is not small enough. Once a solution is found
43       * that generates a threshold below this value, the algorithm will stop.
44       * The stop threshold can be used to prevent the LMedS algorithm iterating
45       * too many times in cases where samples have a very similar accuracy.
46       * For instance, in cases where proportion of outliers is very small (close
47       * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
48       * iterate for a long time trying to find the best solution when indeed
49       * there is no need to do that if a reasonable threshold has already been
50       * reached.
51       * Because of this behaviour the stop threshold can be set to a value much
52       * lower than the one typically used in RANSAC, and yet the algorithm could
53       * still produce even smaller thresholds in estimated results.
54       */
55      public static final double DEFAULT_STOP_THRESHOLD = 1e-3;
56  
57      /**
58       * Minimum allowed stop threshold value.
59       */
60      public static final double MIN_STOP_THRESHOLD = 0.0;
61  
62      /**
63       * Threshold to be used to keep the algorithm iterating in case that best
64       * estimated threshold using median of residuals is not small enough. Once
65       * a solution is found that generates a threshold below this value, the
66       * algorithm will stop.
67       * The stop threshold can be used to prevent the LMedS algorithm iterating
68       * too many times in cases where samples have a very similar accuracy.
69       * For instance, in cases where proportion of outliers is very small (close
70       * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
71       * iterate for a long time trying to find the best solution when indeed
72       * there is no need to do that if a reasonable threshold has already been
73       * reached.
74       * Because of this behaviour the stop threshold can be set to a value much
75       * lower than the one typically used in RANSAC, and yet the algorithm could
76       * still produce even smaller thresholds in estimated results.
77       */
78      private double stopThreshold;
79  
80      /**
81       * Constructor.
82       */
83      public LMedSRobustSinglePoint3DTriangulator() {
84          super();
85          stopThreshold = DEFAULT_STOP_THRESHOLD;
86      }
87  
88      /**
89       * Constructor.
90       *
91       * @param listener listener to be notified of events such as when estimation
92       *                 starts, ends or its progress significantly changes.
93       */
94      public LMedSRobustSinglePoint3DTriangulator(final RobustSinglePoint3DTriangulatorListener listener) {
95          super(listener);
96          stopThreshold = DEFAULT_STOP_THRESHOLD;
97      }
98  
99      /**
100      * Constructor.
101      *
102      * @param points  Matched 2D points. Each point in the list is assumed to be
103      *                projected by the corresponding camera in the list.
104      * @param cameras List of cameras associated to the matched 2D point on the
105      *                same position as the camera on the list.
106      * @throws IllegalArgumentException if provided lists don't have the same
107      *                                  length or their length is less than 2 views, which is the minimum
108      *                                  required to compute triangulation.
109      */
110     public LMedSRobustSinglePoint3DTriangulator(final List<Point2D> points, final List<PinholeCamera> cameras) {
111         super(points, cameras);
112         stopThreshold = DEFAULT_STOP_THRESHOLD;
113     }
114 
115     /**
116      * Constructor.
117      *
118      * @param points   Matched 2D points. Each point in the list is assumed to be
119      *                 projected by the corresponding camera in the list.
120      * @param cameras  List of cameras associated to the matched 2D point on the
121      *                 same position as the camera on the list.
122      * @param listener listener to be notified of events such as when estimation
123      *                 starts, ends or its progress significantly changes.
124      * @throws IllegalArgumentException if provided lists don't have the same
125      *                                  length or their length is less than 2 views, which is the minimum
126      *                                  required to compute triangulation.
127      */
128     public LMedSRobustSinglePoint3DTriangulator(final List<Point2D> points,
129                                                 final List<PinholeCamera> cameras,
130                                                 final RobustSinglePoint3DTriangulatorListener listener) {
131         super(points, cameras, listener);
132         stopThreshold = DEFAULT_STOP_THRESHOLD;
133     }
134 
135     /**
136      * Returns threshold to be used to keep the algorithm iterating in case that
137      * best estimated threshold using median of residuals is not small enough.
138      * Once a solution is found that generates a threshold below this value, the
139      * algorithm will stop.
140      * The stop threshold can be used to prevent the LMedS algorithm iterating
141      * too many times in cases where samples have a very similar accuracy.
142      * For instance, in cases where proportion of outliers is very small (close
143      * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
144      * iterate for a long time trying to find the best solution when indeed
145      * there is no need to do that if a reasonable threshold has already been
146      * reached.
147      * Because of this behaviour the stop threshold can be set to a value much
148      * lower than the one typically used in RANSAC, and yet the algorithm could
149      * still produce even smaller thresholds in estimated results.
150      *
151      * @return stop threshold to stop the algorithm prematurely when a certain
152      * accuracy has been reached.
153      */
154     public double getStopThreshold() {
155         return stopThreshold;
156     }
157 
158     /**
159      * Sets threshold to be used to keep the algorithm iterating in case that
160      * best estimated threshold using median of residuals is not small enough.
161      * Once a solution is found that generates a threshold below this value, the
162      * algorithm will stop.
163      * The stop threshold can be used to prevent the LMedS algorithm iterating
164      * too many times in cases where samples have a very similar accuracy.
165      * For instance, in cases where proportion of outliers is very small (close
166      * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
167      * iterate for a long time trying to find the best solution when indeed
168      * there is no need to do that if a reasonable threshold has already been
169      * reached.
170      * Because of this behaviour the stop threshold can be set to a value much
171      * lower than the one typically used in RANSAC, and yet the algorithm could
172      * still produce even smaller thresholds in estimated results.
173      *
174      * @param stopThreshold stop threshold to stop the algorithm prematurely
175      *                      when a certain accuracy has been reached.
176      * @throws IllegalArgumentException if provided value is zero or negative.
177      * @throws LockedException          if robust estimator is locked because an
178      *                                  estimation is already in progress.
179      */
180     public void setStopThreshold(final double stopThreshold) throws LockedException {
181         if (isLocked()) {
182             throw new LockedException();
183         }
184         if (stopThreshold <= MIN_STOP_THRESHOLD) {
185             throw new IllegalArgumentException();
186         }
187 
188         this.stopThreshold = stopThreshold;
189     }
190 
191     /**
192      * Triangulates provided matched 2D points being projected by each
193      * corresponding camera into a single 3D point.
194      * At least 2 matched 2D points and their corresponding 2 cameras are
195      * required to compute triangulation. If more views are provided, an
196      * averaged solution can be found.
197      *
198      * @return computed triangulated 3D point.
199      * @throws LockedException          if this instance is locked.
200      * @throws NotReadyException        if lists of points and cameras don't have the
201      *                                  same length or less than 2 views are provided.
202      * @throws RobustEstimatorException if estimation fails for any reason
203      *                                  (i.e. numerical instability, no solution available, etc).
204      */
205     @SuppressWarnings("DuplicatedCode")
206     @Override
207     public Point3D triangulate() throws LockedException, NotReadyException, RobustEstimatorException {
208         if (isLocked()) {
209             throw new LockedException();
210         }
211         if (!isReady()) {
212             throw new NotReadyException();
213         }
214 
215         final var innerEstimator = new LMedSRobustEstimator<Point3D>(new LMedSRobustEstimatorListener<>() {
216 
217             // point to be reused when computing residuals
218             private final Point2D testPoint = Point2D.create(CoordinatesType.HOMOGENEOUS_COORDINATES);
219 
220             // non-robust 3D point triangulator
221             private final SinglePoint3DTriangulator triangulator = SinglePoint3DTriangulator.create(
222                     useHomogeneousSolution ? Point3DTriangulatorType.LMSE_HOMOGENEOUS_TRIANGULATOR
223                             : Point3DTriangulatorType.LMSE_INHOMOGENEOUS_TRIANGULATOR);
224 
225             // subset of 2D points
226             private final List<Point2D> subsetPoints = new ArrayList<>();
227 
228             // subst of cameras
229             private final List<PinholeCamera> subsetCameras = new ArrayList<>();
230 
231             @Override
232             public int getTotalSamples() {
233                 return points2D.size();
234             }
235 
236             @Override
237             public int getSubsetSize() {
238                 return MIN_REQUIRED_VIEWS;
239             }
240 
241             @Override
242             public void estimatePreliminarSolutions(final int[] samplesIndices, final List<Point3D> solutions) {
243                 subsetPoints.clear();
244                 subsetPoints.add(points2D.get(samplesIndices[0]));
245                 subsetPoints.add(points2D.get(samplesIndices[1]));
246 
247                 subsetCameras.clear();
248                 subsetCameras.add(cameras.get(samplesIndices[0]));
249                 subsetCameras.add(cameras.get(samplesIndices[1]));
250 
251                 try {
252                     triangulator.setPointsAndCameras(subsetPoints, subsetCameras);
253                     final var triangulated = triangulator.triangulate();
254                     solutions.add(triangulated);
255                 } catch (final Exception e) {
256                     // if anything fails, no solution is added
257                 }
258             }
259 
260             @Override
261             public double computeResidual(final Point3D currentEstimation, final int i) {
262                 final var point2D = points2D.get(i);
263                 final var camera = cameras.get(i);
264 
265                 // project estimated point with camera
266                 camera.project(currentEstimation, testPoint);
267 
268                 // return distance of projected point respect to the original one
269                 // as a residual
270                 return testPoint.distanceTo(point2D);
271             }
272 
273             @Override
274             public boolean isReady() {
275                 return LMedSRobustSinglePoint3DTriangulator.this.isReady();
276             }
277 
278             @Override
279             public void onEstimateStart(final RobustEstimator<Point3D> estimator) {
280                 if (listener != null) {
281                     listener.onTriangulateStart(LMedSRobustSinglePoint3DTriangulator.this);
282                 }
283             }
284 
285             @Override
286             public void onEstimateEnd(final RobustEstimator<Point3D> estimator) {
287                 if (listener != null) {
288                     listener.onTriangulateEnd(LMedSRobustSinglePoint3DTriangulator.this);
289                 }
290             }
291 
292             @Override
293             public void onEstimateNextIteration(final RobustEstimator<Point3D> estimator, final int iteration) {
294                 if (listener != null) {
295                     listener.onTriangulateNextIteration(
296                             LMedSRobustSinglePoint3DTriangulator.this, iteration);
297                 }
298             }
299 
300             @Override
301             public void onEstimateProgressChange(final RobustEstimator<Point3D> estimator, final float progress) {
302                 if (listener != null) {
303                     listener.onTriangulateProgressChange(
304                             LMedSRobustSinglePoint3DTriangulator.this, progress);
305                 }
306             }
307         });
308 
309         try {
310             locked = true;
311             innerEstimator.setConfidence(confidence);
312             innerEstimator.setMaxIterations(maxIterations);
313             innerEstimator.setProgressDelta(progressDelta);
314             innerEstimator.setStopThreshold(stopThreshold);
315             return innerEstimator.estimate();
316         } catch (final com.irurueta.numerical.LockedException e) {
317             throw new LockedException(e);
318         } catch (final com.irurueta.numerical.NotReadyException e) {
319             throw new NotReadyException(e);
320         } finally {
321             locked = false;
322         }
323     }
324 
325     /**
326      * Returns method being used for robust estimation.
327      *
328      * @return method being used for robust estimation.
329      */
330     @Override
331     public RobustEstimatorMethod getMethod() {
332         return RobustEstimatorMethod.LMEDS;
333     }
334 }