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.calibration.estimators;
17
18 import com.irurueta.algebra.AlgebraException;
19 import com.irurueta.algebra.Matrix;
20 import com.irurueta.algebra.Utils;
21 import com.irurueta.ar.calibration.RadialDistortion;
22 import com.irurueta.ar.calibration.RadialDistortionException;
23 import com.irurueta.geometry.Point2D;
24 import com.irurueta.geometry.estimators.LockedException;
25 import com.irurueta.geometry.estimators.NotReadyException;
26 import com.irurueta.numerical.robust.WeightSelection;
27 import com.irurueta.sorting.SortingException;
28
29 import java.util.List;
30
31 /**
32 * This class implements a radial distortion estimator using a weighted
33 * algorithm and correspondences.
34 * Weights can be used so that correspondences assumed to have a better quality
35 * are considered to be more relevant.
36 */
37 public class WeightedRadialDistortionEstimator extends RadialDistortionEstimator {
38 /**
39 * Default number of points (i.e. correspondences) to be weighted and taken
40 * into account.
41 */
42 public static final int DEFAULT_MAX_POINTS = 50;
43
44 /**
45 * Indicates if weights are sorted by default so that largest weighted
46 * correspondences are used first.
47 */
48 public static final boolean DEFAULT_SORT_WEIGHTS = true;
49
50 /**
51 * Maximum number of points (i.e. correspondences) to be weighted and taken
52 * into account.
53 */
54 private int maxPoints;
55
56 /**
57 * Indicates if weights are sorted by default so that largest weighted
58 * correspondences are used first.
59 */
60 private boolean sortWeights;
61
62 /**
63 * Array containing weights for all point correspondences.
64 */
65 private double[] weights;
66
67 /**
68 * Constructor.
69 */
70 public WeightedRadialDistortionEstimator() {
71 super();
72 maxPoints = DEFAULT_MAX_POINTS;
73 sortWeights = DEFAULT_SORT_WEIGHTS;
74 }
75
76 /**
77 * Constructor with listener.
78 * @param listener listener to be notified of events such as when estimation
79 * starts, ends or estimation progress changes.
80 */
81 public WeightedRadialDistortionEstimator(final RadialDistortionEstimatorListener listener) {
82 super(listener);
83 maxPoints = DEFAULT_MAX_POINTS;
84 sortWeights = DEFAULT_SORT_WEIGHTS;
85 }
86
87 /**
88 * Constructor.
89 * @param distortedPoints list of distorted points. Distorted points are
90 * obtained after radial distortion is applied to an undistorted point.
91 * @param undistortedPoints list of undistorted points.
92 * @param weights array containing a weight amount for each correspondence.
93 * The larger the value of a weight, the most significant the
94 * correspondence will be.
95 * @throws IllegalArgumentException if provided lists of points don't have
96 * the same size or their size is smaller than
97 * MIN_NUMBER_OF_POINT_CORRESPONDENCES.
98 */
99 public WeightedRadialDistortionEstimator(final List<Point2D> distortedPoints, final List<Point2D> undistortedPoints,
100 final double[] weights) {
101 super();
102 internalSetPointsAndWeights(distortedPoints, undistortedPoints, weights);
103 maxPoints = DEFAULT_MAX_POINTS;
104 sortWeights = DEFAULT_SORT_WEIGHTS;
105 }
106
107 /**
108 * Constructor.
109 * @param distortedPoints list of distorted points. Distorted points are
110 * obtained after radial distortion is applied to an undistorted point.
111 * @param undistortedPoints list of undistorted points.
112 * @param weights array containing a weight amount for each correspondence.
113 * The larger the value of a weight, the most significant the
114 * correspondence will be.
115 * @param listener listener to be notified of events such as when estimation
116 * starts, ends or estimation progress changes.
117 * @throws IllegalArgumentException if provided lists of points don't have
118 * the same size or their size is smaller than
119 * MIN_NUMBER_OF_POINT_CORRESPONDENCES.
120 */
121
122 public WeightedRadialDistortionEstimator(final List<Point2D> distortedPoints, final List<Point2D> undistortedPoints,
123 final double[] weights, final RadialDistortionEstimatorListener listener) {
124 super(listener);
125 internalSetPointsAndWeights(distortedPoints, undistortedPoints, weights);
126 maxPoints = DEFAULT_MAX_POINTS;
127 sortWeights = DEFAULT_SORT_WEIGHTS;
128 }
129
130 /**
131 * Constructor with distortion center.
132 * @param distortionCenter Distortion center. This is usually equal to the
133 * principal point of an estimated camera. If not set it is assumed to be at
134 * the origin of coordinates (0,0).
135 */
136 public WeightedRadialDistortionEstimator(final Point2D distortionCenter) {
137 super(distortionCenter);
138 maxPoints = DEFAULT_MAX_POINTS;
139 sortWeights = DEFAULT_SORT_WEIGHTS;
140 }
141
142 /**
143 * Constructor with listener and distortion center.
144 * @param distortionCenter Distortion center. This is usually equal to the
145 * principal point of an estimated camera. If not set it is assumed to be at
146 * the origin of coordinates (0,0).
147 * @param listener listener to be notified of events such as when estimation
148 * starts, ends or estimation progress changes.
149 */
150 public WeightedRadialDistortionEstimator(final Point2D distortionCenter,
151 final RadialDistortionEstimatorListener listener) {
152 super(distortionCenter, listener);
153 maxPoints = DEFAULT_MAX_POINTS;
154 sortWeights = DEFAULT_SORT_WEIGHTS;
155 }
156
157 /**
158 * Constructor with points and distortion center.
159 * @param distortedPoints list of distorted points. Distorted points are
160 * obtained after radial distortion is applied to an undistorted point.
161 * @param undistortedPoints list of undistorted points.
162 * @param weights array containing a weight amount for each correspondence.
163 * The larger the value of a weight, the most significant the
164 * correspondence will be.
165 * @param distortionCenter Distortion center. This is usually equal to the
166 * principal point of an estimated camera. If not set it is assumed to be at
167 * the origin of coordinates (0,0).
168 * @throws IllegalArgumentException if provided lists of points don't have
169 * the same size or their size is smaller than
170 * MIN_NUMBER_OF_POINT_CORRESPONDENCES.
171 */
172 public WeightedRadialDistortionEstimator(final List<Point2D> distortedPoints, final List<Point2D> undistortedPoints,
173 final double[] weights, final Point2D distortionCenter) {
174 super(distortionCenter);
175 internalSetPointsAndWeights(distortedPoints, undistortedPoints, weights);
176 maxPoints = DEFAULT_MAX_POINTS;
177 sortWeights = DEFAULT_SORT_WEIGHTS;
178 }
179
180 /**
181 * Constructor.
182 * @param distortedPoints list of distorted points. Distorted points are
183 * obtained after radial distortion is applied to an undistorted point.
184 * @param undistortedPoints list of undistorted points.
185 * @param weights array containing a weight amount for each correspondence.
186 * The larger the value of a weight, the most significant the
187 * correspondence will be.
188 * @param distortionCenter Distortion center. This is usually equal to the
189 * principal point of an estimated camera. If not set it is assumed to be at
190 * the origin of coordinates (0,0).
191 * @param listener listener to be notified of events such as when estimation
192 * starts, ends or estimation progress changes.
193 * @throws IllegalArgumentException if provided lists of points don't have
194 * the same size or their size is smaller than
195 * MIN_NUMBER_OF_POINT_CORRESPONDENCES.
196 */
197 public WeightedRadialDistortionEstimator(final List<Point2D> distortedPoints, final List<Point2D> undistortedPoints,
198 final double[] weights, final Point2D distortionCenter,
199 final RadialDistortionEstimatorListener listener) {
200 super(distortionCenter, listener);
201 internalSetPointsAndWeights(distortedPoints, undistortedPoints, weights);
202 maxPoints = DEFAULT_MAX_POINTS;
203 sortWeights = DEFAULT_SORT_WEIGHTS;
204 }
205
206 /**
207 * Sets lists of corresponding distorted and undistorted points.
208 * @param distortedPoints list of distorted points. Distorted points are
209 * obtained after radial distortion is applied to an undistorted point.
210 * @param undistortedPoints list of undistorted points.
211 * @param weights array containing a weight amount for each correspondence.
212 * The larger the value of a weight, the most significant the
213 * correspondence will be.
214 * @throws LockedException if estimator is locked.
215 * @throws IllegalArgumentException if any of the lists or arrays are null
216 * or if provided lists of points don't have the same size and enough points
217 * or if the length of the weights array is not equal to the number of point
218 * correspondences.
219 */
220 public void setPointsAndWeights(final List<Point2D> distortedPoints, final List<Point2D> undistortedPoints,
221 final double[] weights) throws LockedException {
222 if (isLocked()) {
223 throw new LockedException();
224 }
225
226 internalSetPointsAndWeights(distortedPoints, undistortedPoints, weights);
227 }
228
229 /**
230 * Indicates if lists of corresponding distorted and undistorted points are
231 * valid.
232 * Lists are considered valid if they have the same number of points and
233 * both have more than the required minimum of correspondences (which is 2).
234 * @param distortedPoints list of distorted points. Distorted points are
235 * obtained after radial distortion is applied to an undistorted point.
236 * @param undistortedPoints list of undistorted points.
237 * @param weights array containing a weight amount for each correspondence.
238 * The larger the value of a weight, the most significant the
239 * correspondence will be.
240 * @return true if lists of points are valid, false otherwise.
241 */
242 public boolean areValidPointsAndWeights(final List<Point2D> distortedPoints, final List<Point2D> undistortedPoints,
243 final double[] weights){
244 if (distortedPoints == null || undistortedPoints == null || weights == null) {
245 return false;
246 }
247
248 return distortedPoints.size() == undistortedPoints.size() && distortedPoints.size() == weights.length
249 && distortedPoints.size() >= getMinNumberOfMatchedPoints();
250 }
251
252 /**
253 * Returns array containing a weight amount for each correspondence.
254 * The larger the value of a weight, the most significant the
255 * correspondence will be.
256 * @return array containing weights for each correspondence.
257 */
258 public double[] getWeights() {
259 return weights;
260 }
261
262 /**
263 * Returns boolean indicating whether weights have been provided and are
264 * available for retrieval.
265 * @return true if weights are available, false otherwise.
266 */
267 public boolean areWeightsAvailable() {
268 return weights != null;
269 }
270
271 /**
272 * Returns maximum number of points (i.e. correspondences) to be weighted
273 * and taken into account.
274 * @return maximum number of points to be weighted.
275 */
276 public int getMaxPoints() {
277 return maxPoints;
278 }
279
280 /**
281 * Sets maximum number of points (i.e. correspondences) to be weighted and
282 * taken into account.
283 * @param maxPoints maximum number of points to be weighted.
284 * @throws IllegalArgumentException if provided value is less than the
285 * minimum allowed number of point correspondences.
286 * @throws LockedException if this instance is locked.
287 */
288 public void setMaxPoints(final int maxPoints) throws LockedException {
289 if (isLocked()) {
290 throw new LockedException();
291 }
292 if (maxPoints < getMinNumberOfMatchedPoints()) {
293 throw new IllegalArgumentException();
294 }
295
296 this.maxPoints = maxPoints;
297 }
298
299 /**
300 * Indicates if weights are sorted by so that largest weighted
301 * correspondences are used first.
302 * @return true if weights are sorted, false otherwise.
303 */
304 public boolean isSortWeightsEnabled() {
305 return sortWeights;
306 }
307
308 /**
309 * Specifies whether weights are sorted by so that largest weighted
310 * correspondences are used first.
311 * @param sortWeights true if weights are sorted, false otherwise.
312 * @throws LockedException if this instance is locked.
313 */
314 public void setSortWeightsEnabled(final boolean sortWeights) throws LockedException {
315 if (isLocked()) {
316 throw new LockedException();
317 }
318
319 this.sortWeights = sortWeights;
320 }
321
322 /**
323 * Indicates if this estimator is ready to start the estimation.
324 * Estimator will be ready once both lists and weights are available.
325 * @return true if estimator is ready, false otherwise.
326 */
327 @Override
328 public boolean isReady() {
329 return arePointsAvailable() && areWeightsAvailable();
330 }
331
332 /**
333 * Estimates a radial distortion.
334 * @return estimated radial distortion.
335 * @throws LockedException if estimator is locked.
336 * @throws NotReadyException if input has not yet been provided.
337 * @throws RadialDistortionEstimatorException if an error occurs during
338 * estimation, usually because input data is not valid.
339 */
340 @SuppressWarnings("DuplicatedCode")
341 @Override
342 public RadialDistortion estimate() throws LockedException, NotReadyException, RadialDistortionEstimatorException {
343 if (isLocked()) {
344 throw new LockedException();
345 }
346 if (!isReady()) {
347 throw new NotReadyException();
348 }
349
350 try {
351 locked = true;
352 if (listener != null) {
353 listener.onEstimateStart(this);
354 }
355
356 final var selection = WeightSelection.selectWeights(weights, sortWeights, maxPoints);
357 final var selected = selection.getSelected();
358
359 final var nPoints = distortedPoints.size();
360
361 final var aMatrix = new Matrix(2 * nPoints, numKParams);
362 final var b = new double[2 * nPoints];
363
364 final var iteratorDistorted = distortedPoints.iterator();
365 final var iteratorUndistorted = undistortedPoints.iterator();
366
367 Point2D distorted;
368 Point2D undistorted;
369 var index = 0;
370 var counter = 0;
371
372 // undistorted normalized homogeneous coordinates
373 double uNormHomX;
374 double uNormHomY;
375 double uNormHomW;
376 // undistorted normalized inhomogeneous coordinates
377 double uNormInhomX;
378 double uNormInhomY;
379 // undistorted denormalized homogeneous coordinates
380 double uDenormHomX;
381 double uDenormHomY;
382 double uDenormHomW;
383 // undistorted denormalized inhomogeneous coordinates
384 double uDenormInhomX;
385 double uDenormInhomY;
386 // distorted inhomogeneous coordinates
387 double dInhomX;
388 double dInhomY;
389 double rowNormX;
390 double rowNormY;
391
392 // radial distortion center
393 var centerX = 0.0;
394 var centerY = 0.0;
395 if (distortionCenter != null) {
396 centerX = distortionCenter.getInhomX();
397 centerY = distortionCenter.getInhomY();
398 }
399
400 // radial distance of undistorted normalized (calibration independent)
401 // coordinates
402 double r2;
403 double a;
404 double value;
405 double weight;
406
407 while (iteratorDistorted.hasNext() && iteratorUndistorted.hasNext()) {
408 distorted = iteratorDistorted.next();
409 undistorted = iteratorUndistorted.next();
410
411 if (selected[index]) {
412 undistorted.normalize();
413
414 weight = weights[index];
415
416 uDenormHomX = undistorted.getHomX();
417 uDenormHomY = undistorted.getHomY();
418 uDenormHomW = undistorted.getHomW();
419
420 uDenormInhomX = uDenormHomX / uDenormHomW;
421 uDenormInhomY = uDenormHomY / uDenormHomW;
422
423 // multiply intrinsic parameters by undistorted point
424 uNormHomX = kInv.getElementAt(0, 0) * uDenormHomX
425 + kInv.getElementAt(0, 1) * uDenormHomY
426 + kInv.getElementAt(0, 2) * uDenormHomW;
427 uNormHomY = kInv.getElementAt(1, 0) * uDenormHomX
428 + kInv.getElementAt(1, 1) * uDenormHomY
429 + kInv.getElementAt(1, 2) * uDenormHomW;
430 uNormHomW = kInv.getElementAt(2, 0) * uDenormHomX
431 + kInv.getElementAt(2, 1) * uDenormHomY
432 + kInv.getElementAt(2, 2) * uDenormHomW;
433
434 uNormInhomX = uNormHomX / uNormHomW;
435 uNormInhomY = uNormHomY / uNormHomW;
436
437 r2 = uNormInhomX * uNormInhomX + uNormInhomY * uNormInhomY;
438
439 dInhomX = distorted.getInhomX();
440 dInhomY = distorted.getInhomY();
441
442 a = 1.0;
443 rowNormX = rowNormY = 0.0;
444 for (var i = 0; i < numKParams; i++) {
445 a *= r2;
446
447 // x and y coordinates generate linear dependent equations, for
448 // that reason we need more than one point
449
450 // x coordinates
451 value = (uDenormInhomX - centerX) * a * weight;
452 aMatrix.setElementAt(counter * 2, i, value);
453
454 rowNormX += Math.pow(value, 2.0);
455
456 // y coordinates
457 value = (uDenormInhomY - centerY) * a * weight;
458 aMatrix.setElementAt(counter * 2 + 1, i, value);
459
460 rowNormY += Math.pow(value, 2.0);
461 }
462
463 // x coordinates
464 value = (dInhomX - uDenormInhomX) * weight;
465 b[counter * 2] = value;
466
467 rowNormX += Math.pow(value, 2.0);
468
469 // y coordinates
470 value = (dInhomY - uDenormInhomY) * weight;
471 b[counter * 2 + 1] = value;
472
473 rowNormY += Math.pow(value, 2.0);
474
475 // normalize rows to increase accuracy
476 for (var i = 0; i < numKParams; i++) {
477 aMatrix.setElementAt(counter * 2, i,
478 aMatrix.getElementAt(counter * 2, i) / rowNormX);
479 aMatrix.setElementAt(counter * 2 + 1, i,
480 aMatrix.getElementAt(counter * 2 + 1, i) / rowNormY);
481 }
482
483 b[counter * 2] /= rowNormX;
484 b[counter * 2 +1] /= rowNormY;
485
486 counter++;
487 }
488
489 index++;
490 }
491
492 final var params = Utils.solve(aMatrix, b);
493
494 final var distortion = new RadialDistortion(params, distortionCenter, horizontalFocalLength,
495 verticalFocalLength, skew);
496
497 if (listener != null) {
498 listener.onEstimateEnd(this);
499 }
500
501 return distortion;
502 } catch (final AlgebraException | SortingException | RadialDistortionException e) {
503 throw new RadialDistortionEstimatorException(e);
504 } finally {
505 locked = false;
506 }
507 }
508
509 /**
510 * Returns type of radial distortion estimator.
511 * @return type of radial distortion estimator.
512 */
513 @Override
514 public RadialDistortionEstimatorType getType() {
515 return RadialDistortionEstimatorType.WEIGHTED_RADIAL_DISTORTION_ESTIMATOR;
516 }
517
518 /**
519 * Internal method to set list of corresponding points (it does not check
520 * if estimator is locked).
521 * @param distortedPoints list of distorted points. Distorted points are
522 * obtained after radial distortion is applied to an undistorted point.
523 * @param undistortedPoints list of undistorted points.
524 * @param weights array containing a weight amount for each correspondence.
525 * The larger the value of a weight, the most significant the
526 * correspondence will be.
527 * @throws IllegalArgumentException if any of the lists or arrays are null
528 * or if provided lists of points don't have the same size and enough points
529 * or if the length of the weights array is not equal to the number of point
530 * correspondences.
531 */
532 private void internalSetPointsAndWeights(final List<Point2D> distortedPoints, final List<Point2D> undistortedPoints,
533 final double[] weights) {
534
535 if (distortedPoints == null || undistortedPoints == null || weights == null) {
536 throw new IllegalArgumentException();
537 }
538
539 if (!areValidPointsAndWeights(distortedPoints, undistortedPoints, weights)) {
540 throw new IllegalArgumentException();
541 }
542
543 this.distortedPoints = distortedPoints;
544 this.undistortedPoints = undistortedPoints;
545 this.weights = weights;
546 }
547 }