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.geometry.estimators;
17
18 import com.irurueta.geometry.AffineTransformation2D;
19 import com.irurueta.geometry.CoincidentPointsException;
20 import com.irurueta.geometry.CoordinatesType;
21 import com.irurueta.geometry.Point2D;
22 import com.irurueta.numerical.robust.LMedSRobustEstimator;
23 import com.irurueta.numerical.robust.LMedSRobustEstimatorListener;
24 import com.irurueta.numerical.robust.RobustEstimator;
25 import com.irurueta.numerical.robust.RobustEstimatorException;
26 import com.irurueta.numerical.robust.RobustEstimatorMethod;
27
28 import java.util.List;
29
30 /**
31 * Finds the best affine 2D transformation for provided collections of matched
32 * 2D points using LMedS algorithm.
33 */
34 public class LMedSPointCorrespondenceAffineTransformation2DRobustEstimator
35 extends PointCorrespondenceAffineTransformation2DRobustEstimator {
36
37 /**
38 * Default value to be used for stop threshold. Stop threshold can be used
39 * to keep the algorithm iterating in case that best estimated threshold
40 * using median of residuals is not small enough. Once a solution is found
41 * that generates a threshold below this value, the algorithm will stop.
42 * The stop threshold can be used to prevent the LMedS algorithm iterating
43 * too many times in cases where samples have a very similar accuracy.
44 * For instance, in cases where proportion of outliers is very small (close
45 * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
46 * iterate for a long time trying to find the best solution when indeed
47 * there is no need to do that if a reasonable threshold has already been
48 * reached.
49 * Because of this behaviour the stop threshold can be set to a value much
50 * lower than the one typically used in RANSAC, and yet the algorithm could
51 * still produce even smaller thresholds in estimated results.
52 */
53 public static final double DEFAULT_STOP_THRESHOLD = 1.0;
54
55 /**
56 * Minimum allowed stop threshold value.
57 */
58 public static final double MIN_STOP_THRESHOLD = 0.0;
59
60 /**
61 * Threshold to be used to keep the algorithm iterating in case that best
62 * estimated threshold using median of residuals is not small enough. Once
63 * a solution is found that generates a threshold below this value, the
64 * algorithm will stop.
65 * The stop threshold can be used to prevent the LMedS algorithm iterating
66 * too many times in cases where samples have a very similar accuracy.
67 * For instance, in cases where proportion of outliers is very small (close
68 * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
69 * iterate for a long time trying to find the best solution when indeed
70 * there is no need to do that if a reasonable threshold has already been
71 * reached.
72 * Because of this behaviour the stop threshold can be set to a value much
73 * lower than the one typically used in RANSAC, and yet the algorithm could
74 * still produce even smaller thresholds in estimated results.
75 */
76 private double stopThreshold;
77
78 /**
79 * Constructor.
80 */
81 public LMedSPointCorrespondenceAffineTransformation2DRobustEstimator() {
82 super();
83 stopThreshold = DEFAULT_STOP_THRESHOLD;
84 }
85
86 /**
87 * Constructor with lists of points to be used to estimate an affine 2D
88 * transformation.
89 * Points in the list located at the same position are considered to be
90 * matched. Hence, both lists must have the same size, and their size must
91 * be greater or equal than MINIMUM_SIZE.
92 *
93 * @param inputPoints list of input points to be used to estimate an
94 * affine 2D transformation.
95 * @param outputPoints list of output points to be used to estimate an
96 * affine 2D transformation.
97 * @throws IllegalArgumentException if provided lists of points don't have
98 * the same size or their size is smaller than MINIMUM_SIZE.
99 */
100 public LMedSPointCorrespondenceAffineTransformation2DRobustEstimator(
101 final List<Point2D> inputPoints, final List<Point2D> outputPoints) {
102 super(inputPoints, outputPoints);
103 stopThreshold = DEFAULT_STOP_THRESHOLD;
104 }
105
106 /**
107 * Constructor.
108 *
109 * @param listener listener to be notified of events such as when estimation
110 * starts, ends or its progress significantly changes.
111 */
112 public LMedSPointCorrespondenceAffineTransformation2DRobustEstimator(
113 final AffineTransformation2DRobustEstimatorListener listener) {
114 super(listener);
115 stopThreshold = DEFAULT_STOP_THRESHOLD;
116 }
117
118 /**
119 * Constructor with listener and lists of points to be used to estimate an
120 * affine 2D transformation.
121 * Points in the list located at the same position are considered to be
122 * matched. Hence, both lists must have the same size, and their size must
123 * be greater or equal than MINIMUM_SIZE.
124 *
125 * @param listener listener to be notified of events such as when estimation
126 * starts, ends or its progress significantly changes.
127 * @param inputPoints list of input points to be used to estimate an
128 * affine 2D transformation.
129 * @param outputPoints list of output points to be used to estimate an
130 * affine 2D transformation.
131 * @throws IllegalArgumentException if provided lists of points don't have
132 * the same size or their size is smaller than MINIMUM_SIZE.
133 */
134 public LMedSPointCorrespondenceAffineTransformation2DRobustEstimator(
135 final AffineTransformation2DRobustEstimatorListener listener,
136 final List<Point2D> inputPoints, final List<Point2D> outputPoints) {
137 super(listener, inputPoints, outputPoints);
138 stopThreshold = DEFAULT_STOP_THRESHOLD;
139 }
140
141 /**
142 * Returns threshold to be used to keep the algorithm iterating in case that
143 * best estimated threshold using median of residuals is not small enough.
144 * Once a solution is found that generates a threshold below this value, the
145 * algorithm will stop.
146 * The stop threshold can be used to prevent the LMedS algorithm iterating
147 * too many times in cases where samples have a very similar accuracy.
148 * For instance, in cases where proportion of outliers is very small (close
149 * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
150 * iterate for a long time trying to find the best solution when indeed
151 * there is no need to do that if a reasonable threshold has already been
152 * reached.
153 * Because of this behaviour the stop threshold can be set to a value much
154 * lower than the one typically used in RANSAC, and yet the algorithm could
155 * still produce even smaller thresholds in estimated results.
156 *
157 * @return stop threshold to stop the algorithm prematurely when a certain
158 * accuracy has been reached.
159 */
160 public double getStopThreshold() {
161 return stopThreshold;
162 }
163
164 /**
165 * Sets threshold to be used to keep the algorithm iterating in case that
166 * best estimated threshold using median of residuals is not small enough.
167 * Once a solution is found that generates a threshold below this value, the
168 * algorithm will stop.
169 * The stop threshold can be used to prevent the LMedS algorithm iterating
170 * too many times in cases where samples have a very similar accuracy.
171 * For instance, in cases where proportion of outliers is very small (close
172 * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
173 * iterate for a long time trying to find the best solution when indeed
174 * there is no need to do that if a reasonable threshold has already been
175 * reached.
176 * Because of this behaviour the stop threshold can be set to a value much
177 * lower than the one typically used in RANSAC, and yet the algorithm could
178 * still produce even smaller thresholds in estimated results.
179 *
180 * @param stopThreshold stop threshold to stop the algorithm prematurely
181 * when a certain accuracy has been reached.
182 * @throws IllegalArgumentException if provided value is zero or negative.
183 * @throws LockedException if robust estimator is locked because an
184 * estimation is already in progress.
185 */
186 public void setStopThreshold(final double stopThreshold) throws LockedException {
187 if (isLocked()) {
188 throw new LockedException();
189 }
190 if (stopThreshold <= MIN_STOP_THRESHOLD) {
191 throw new IllegalArgumentException();
192 }
193
194 this.stopThreshold = stopThreshold;
195 }
196
197 /**
198 * Estimates an affine 2D transformation using a robust estimator and
199 * the best set of matched 2D point correspondences found using the robust
200 * estimator.
201 *
202 * @return an affine 2D transformation.
203 * @throws LockedException if robust estimator is locked because an
204 * estimation is already in progress.
205 * @throws NotReadyException if provided input data is not enough to start
206 * the estimation.
207 * @throws RobustEstimatorException if estimation fails for any reason
208 * (i.e. numerical instability, no solution available, etc).
209 */
210 @SuppressWarnings("DuplicatedCode")
211 @Override
212 public AffineTransformation2D estimate() throws LockedException, NotReadyException, RobustEstimatorException {
213 if (isLocked()) {
214 throw new LockedException();
215 }
216 if (!isReady()) {
217 throw new NotReadyException();
218 }
219
220 final var innerEstimator = new LMedSRobustEstimator<>(
221 new LMedSRobustEstimatorListener<AffineTransformation2D>() {
222
223 // point to be reused when computing residuals
224 private final Point2D testPoint = Point2D.create(CoordinatesType.HOMOGENEOUS_COORDINATES);
225
226 @Override
227 public int getTotalSamples() {
228 return inputPoints.size();
229 }
230
231 @Override
232 public int getSubsetSize() {
233 return AffineTransformation2DRobustEstimator.MINIMUM_SIZE;
234 }
235
236 @Override
237 public void estimatePreliminarSolutions(
238 final int[] samplesIndices, final List<AffineTransformation2D> solutions) {
239 final var inputPoint1 = inputPoints.get(samplesIndices[0]);
240 final var inputPoint2 = inputPoints.get(samplesIndices[1]);
241 final var inputPoint3 = inputPoints.get(samplesIndices[2]);
242
243 final var outputPoint1 = outputPoints.get(samplesIndices[0]);
244 final var outputPoint2 = outputPoints.get(samplesIndices[1]);
245 final var outputPoint3 = outputPoints.get(samplesIndices[2]);
246
247 try {
248 final var transformation = new AffineTransformation2D(inputPoint1, inputPoint2, inputPoint3,
249 outputPoint1, outputPoint2, outputPoint3);
250 solutions.add(transformation);
251 } catch (final CoincidentPointsException e) {
252 // if points are coincident, no solution is added
253 }
254 }
255
256 @Override
257 public double computeResidual(final AffineTransformation2D currentEstimation, final int i) {
258 final var inputPoint = inputPoints.get(i);
259 final var outputPoint = outputPoints.get(i);
260
261 // transform input point and store result in mTestPoint
262 currentEstimation.transform(inputPoint, testPoint);
263
264 return outputPoint.distanceTo(testPoint);
265 }
266
267 @Override
268 public boolean isReady() {
269 return LMedSPointCorrespondenceAffineTransformation2DRobustEstimator.this.isReady();
270 }
271
272 @Override
273 public void onEstimateStart(final RobustEstimator<AffineTransformation2D> estimator) {
274 if (mListener != null) {
275 mListener.onEstimateStart(
276 LMedSPointCorrespondenceAffineTransformation2DRobustEstimator.this);
277 }
278 }
279
280 @Override
281 public void onEstimateEnd(final RobustEstimator<AffineTransformation2D> estimator) {
282 if (mListener != null) {
283 mListener.onEstimateEnd(
284 LMedSPointCorrespondenceAffineTransformation2DRobustEstimator.this);
285 }
286 }
287
288 @Override
289 public void onEstimateNextIteration(
290 final RobustEstimator<AffineTransformation2D> estimator, final int iteration) {
291 if (mListener != null) {
292 mListener.onEstimateNextIteration(
293 LMedSPointCorrespondenceAffineTransformation2DRobustEstimator.this,
294 iteration);
295 }
296 }
297
298 @Override
299 public void onEstimateProgressChange(
300 final RobustEstimator<AffineTransformation2D> estimator, final float progress) {
301 if (mListener != null) {
302 mListener.onEstimateProgressChange(
303 LMedSPointCorrespondenceAffineTransformation2DRobustEstimator.this,
304 progress);
305 }
306 }
307 });
308
309 try {
310 locked = true;
311 inliersData = null;
312 innerEstimator.setConfidence(confidence);
313 innerEstimator.setMaxIterations(maxIterations);
314 innerEstimator.setProgressDelta(progressDelta);
315 innerEstimator.setStopThreshold(stopThreshold);
316 final var transformation = innerEstimator.estimate();
317 inliersData = innerEstimator.getInliersData();
318 return attemptRefine(transformation);
319 } catch (final com.irurueta.numerical.LockedException e) {
320 throw new LockedException(e);
321 } catch (final com.irurueta.numerical.NotReadyException e) {
322 throw new NotReadyException(e);
323 } finally {
324 locked = false;
325 }
326 }
327
328 /**
329 * Returns method being used for robust estimation.
330 *
331 * @return method being used for robust estimation.
332 */
333 @Override
334 public RobustEstimatorMethod getMethod() {
335 return RobustEstimatorMethod.LMEDS;
336 }
337
338 /**
339 * Gets standard deviation used for Levenberg-Marquardt fitting during
340 * refinement.
341 * Returned value gives an indication of how much variance each residual
342 * has.
343 * Typically, this value is related to the threshold used on each robust
344 * estimation, since residuals of found inliers are within the range of
345 * such threshold.
346 *
347 * @return standard deviation used for refinement.
348 */
349 @Override
350 protected double getRefinementStandardDeviation() {
351 final var inliersData = (LMedSRobustEstimator.LMedSInliersData) getInliersData();
352
353 // avoid setting a threshold too strict
354 final var threshold = inliersData.getEstimatedThreshold();
355 return Math.max(threshold, stopThreshold);
356 }
357 }