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.geometry.estimators;
17  
18  import com.irurueta.algebra.AlgebraException;
19  import com.irurueta.algebra.ArrayUtils;
20  import com.irurueta.algebra.Matrix;
21  import com.irurueta.algebra.SingularValueDecomposer;
22  import com.irurueta.algebra.Utils;
23  import com.irurueta.geometry.CoincidentPointsException;
24  import com.irurueta.geometry.GeometryException;
25  import com.irurueta.geometry.InhomogeneousPoint3D;
26  import com.irurueta.geometry.MetricTransformation3D;
27  import com.irurueta.geometry.PinholeCamera;
28  import com.irurueta.geometry.PinholeCameraIntrinsicParameters;
29  import com.irurueta.geometry.Point2D;
30  import com.irurueta.geometry.Point3D;
31  import com.irurueta.geometry.Rotation3D;
32  
33  import java.util.ArrayList;
34  import java.util.List;
35  
36  /**
37   * UPnP (Uncalibrated Perspective-n-Point) implementation to estimate pinhole
38   * cameras from 2D/3D point correspondences.
39   * This class besides determining camera pose is also capable to find its focal
40   * length assuming unitary aspect ratio (equal horizontal and vertical focal
41   * lengths) and that the resulting focal length is positive.
42   * This class is an implementation following the one proposed by Adrian
43   * Penate-Senchez et al. on "Exhaustive Linearization for Robust Camera Pose and
44   * Focal Length Estimation".
45   * Paper can be found at:
46   * <a href="http://www.iri.upc.edu/files/scidoc/1404-Exhaustive-linearization-for-robust-camera-pose-and-focal-length-estimation.pdf">
47   *     http://www.iri.upc.edu/files/scidoc/1404-Exhaustive-linearization-for-robust-camera-pose-and-focal-length-estimation.pdf
48   * </a>
49   */
50  @SuppressWarnings("DuplicatedCode")
51  public class UPnPPointCorrespondencePinholeCameraEstimator extends PointCorrespondencePinholeCameraEstimator {
52  
53      /**
54       * Indicates that by default planar configuration is checked to determine
55       * whether point correspondences are in such configuration and find a
56       * specific solution for such case.
57       */
58      public static final boolean DEFAULT_PLANAR_CONFIGURATION_ALLOWED = true;
59  
60      /**
61       * Indicates that by default a dimension 2 null-space is not allowed.
62       */
63      public static final boolean DEFAULT_NULLSPACE_DIMENSION2_ALLOWED = true;
64  
65      /**
66       * Default threshold to determine whether 3D matched points are in a planar
67       * configuration.
68       * Points are considered to be laying in a plane when the smallest singular
69       * value of their covariance matrix has a value much smaller than the second
70       * smallest as many times as this value.
71       */
72      public static final double DEFAULT_PLANAR_THRESHOLD = 1e13;
73  
74      /**
75       * Default skewness value.
76       */
77      public static final double DEFAULT_SKEWNESS = 0.0;
78  
79      /**
80       * Default value for horizontal coordinate of principal point.
81       */
82      public static final double DEFAULT_HORIZONTAL_PRINCIPAL_POINT = 0.0;
83  
84      /**
85       * Default value for vertical coordinate of principal point.
86       */
87      public static final double DEFAULT_VERTICAL_PRINCIPAL_POINT = 0.0;
88  
89      /**
90       * Number of control points used in a general configuration.
91       */
92      private static final int GENERAL_NUM_CONTROL_POINTS = 4;
93  
94      /**
95       * Number of control points used in a planar configuration.
96       */
97      private static final int PLANAR_NUM_CONTROL_POINTS = 3;
98  
99      /**
100      * Indicates whether planar configuration is checked to determine whether
101      * point correspondences are in such configuration and find a specific
102      * solution for such case.
103      */
104     private boolean planarConfigurationAllowed = DEFAULT_PLANAR_CONFIGURATION_ALLOWED;
105 
106     /**
107      * Indicates whether the case where a dimension 2 null-space is allowed.
108      * When allowed, additional constraints are taken into account to ensure
109      * equality of scales so that less point correspondences are required.
110      * Enabling this parameter is usually ok.
111      */
112     private boolean nullspaceDimension2Allowed = DEFAULT_NULLSPACE_DIMENSION2_ALLOWED;
113 
114     /**
115      * Threshold to determine whether 3D matched points are in a planar
116      * configuration.
117      * Points are considered to be laying in a plane when the smallest singular
118      * value of their covariance matrix has a value much smaller than the
119      * largest one as many times as this value.
120      */
121     private double planarThreshold = DEFAULT_PLANAR_THRESHOLD;
122 
123     /**
124      * Skewness value of intrinsic parameters to be used on estimated camera.
125      */
126     private double skewness = DEFAULT_SKEWNESS;
127 
128     /**
129      * Horizontal coordinate of principal point on intrinsic parameters to be
130      * used on estimated camera.
131      */
132     private double horizontalPrincipalPoint = DEFAULT_HORIZONTAL_PRINCIPAL_POINT;
133 
134     /**
135      * Vertical coordinate of principal point on intrinsic parameters to be
136      * used on estimated camera.
137      */
138     private double verticalPrincipalPoint = DEFAULT_VERTICAL_PRINCIPAL_POINT;
139 
140     /**
141      * Indicates whether provided correspondences were found to be laying in a
142      * planar configuration during the estimation.
143      */
144     private boolean isPlanar;
145 
146     /**
147      * Computed control points in world coordinates.
148      */
149     private List<Point3D> controlWorldPoints;
150 
151     /**
152      * Contains barycentric coordinates to express 3D world point in terms of
153      * control points.
154      * For general configuration, each row contains 4 coordinates and alphas
155      * has size nx4, where n is the number of provided 3D world points.
156      * For planar configuration, each row contains 3 coordinates and alphas
157      * has size nx3, where n is the number of provided 3D world points.
158      * Both reference frames are centered in the centroid, alphas can be used
159      * in both world and camera coordinates.
160      */
161     private Matrix alphas;
162 
163     /**
164      * M matrix to find control points in camera coordinates and focal length.
165      * M has size 2*n x 12 (general configuration) or 2*n x 9
166      * (planar configuration), where n is the number of provided 2D observed
167      * points.
168      */
169     private Matrix m;
170 
171     /**
172      * List containing columns of null-space of M. Linear combinations of these
173      * columns contain possible solutions for control points coordinates in
174      * camera reference (up to scale) with z terms normalized by an unknown
175      * focal length.
176      * First item of the list contains last column of v, which corresponds to
177      * the smallest singular value.
178      * Last item of the list contains (column - number of control points) column
179      * of v.
180      */
181     private List<double[]> nullspace;
182 
183     /**
184      * Possible solutions for the estimation.
185      */
186     private List<Solution> solutions;
187 
188     /**
189      * Constructor.
190      */
191     public UPnPPointCorrespondencePinholeCameraEstimator() {
192         super();
193     }
194 
195     /**
196      * Constructor with listener.
197      *
198      * @param listener listener to be notified of events such as when estimation
199      *                 starts, ends or estimation progress changes.
200      */
201     public UPnPPointCorrespondencePinholeCameraEstimator(final PinholeCameraEstimatorListener listener) {
202         super(listener);
203     }
204 
205     /**
206      * Constructor.
207      *
208      * @param points3D list of corresponding 3D points.
209      * @param points2D list of corresponding 2D points.
210      * @throws IllegalArgumentException if any of the lists are null.
211      * @throws WrongListSizesException  if provided lists of points don't have
212      *                                  the same size and enough points.
213      */
214     public UPnPPointCorrespondencePinholeCameraEstimator(
215             final List<Point3D> points3D, final List<Point2D> points2D) throws WrongListSizesException {
216         super();
217         internalSetListsUPnP(points3D, points2D);
218     }
219 
220     /**
221      * Constructor.
222      *
223      * @param points3D list of corresponding 3D points.
224      * @param points2D list of corresponding 2D points.
225      * @param listener listener to be notified of events such as when estimation
226      *                 starts, ends or estimation progress changes.
227      * @throws IllegalArgumentException if any of the lists are null.
228      * @throws WrongListSizesException  if provided lists of points don't have
229      *                                  the same size and enough points.
230      */
231     public UPnPPointCorrespondencePinholeCameraEstimator(
232             final List<Point3D> points3D, final List<Point2D> points2D, final PinholeCameraEstimatorListener listener)
233             throws WrongListSizesException {
234         super(listener);
235         internalSetListsUPnP(points3D, points2D);
236     }
237 
238     /**
239      * Sets list of corresponding points.
240      *
241      * @param points3D list of corresponding 3D points.
242      * @param points2D list of corresponding 2D points.
243      * @throws LockedException          if estimator is locked.
244      * @throws IllegalArgumentException if any of the lists are null.
245      * @throws WrongListSizesException  if provided lists of points don't have
246      *                                  the same size and enough points.
247      */
248     @Override
249     public void setLists(final List<Point3D> points3D, final List<Point2D> points2D)
250             throws LockedException, WrongListSizesException {
251         if (isLocked()) {
252             throw new LockedException();
253         }
254 
255         internalSetListsUPnP(points3D, points2D);
256     }
257 
258     /**
259      * Indicates whether planar configuration is checked to determine whether
260      * point correspondences are in such configuration and find a specific
261      * solution for such case.
262      *
263      * @return true to allow specific solutions for planar configurations,
264      * false to always find a solution assuming the general case.
265      */
266     public boolean isPlanarConfigurationAllowed() {
267         return planarConfigurationAllowed;
268     }
269 
270     /**
271      * Specifies whether planar configuration is checked to determine whether
272      * point correspondences are in such configuration and find a specific
273      * solution for such case.
274      *
275      * @param planarConfigurationAllowed true to allow specific solutions for
276      *                                   planar configurations, false to always find a solution assuming the
277      *                                   general case.
278      * @throws LockedException if estimator is locked.
279      */
280     public void setPlanarConfigurationAllowed(final boolean planarConfigurationAllowed) throws LockedException {
281         if (isLocked()) {
282             throw new LockedException();
283         }
284         this.planarConfigurationAllowed = planarConfigurationAllowed;
285     }
286 
287     /**
288      * Indicates whether the case where a dimension 2 null-space is allowed.
289      * When allowed, additional constraints are taken into account to ensure
290      * equality of scales so that less point correspondences are required.
291      * Enabling this parameter is usually ok.
292      *
293      * @return true to allow 2-dimensional null-space, false otherwise.
294      */
295     public boolean isNullspaceDimension2Allowed() {
296         return nullspaceDimension2Allowed;
297     }
298 
299     /**
300      * Specifies whether the case where a dimension 2 null-space is allowed.
301      * When allowed, additional constraints are taken into account to ensure
302      * equality of scales so that less point correspondences are required.
303      * Enabling this parameter is usually ok.
304      *
305      * @param nullspaceDimension2Allowed true to allow 2-dimensional null-space,
306      *                                   false otherwise.
307      * @throws LockedException if estimator is locked.
308      */
309     public void setNullspaceDimension2Allowed(final boolean nullspaceDimension2Allowed) throws LockedException {
310         if (isLocked()) {
311             throw new LockedException();
312         }
313         this.nullspaceDimension2Allowed = nullspaceDimension2Allowed;
314     }
315 
316     /**
317      * Gets threshold to determine whether 3D matched points are in a planar
318      * configuration.
319      * Points are considered to be laying in a plane when the smallest singular
320      * value of their covariance matrix has a value much smaller than the
321      * largest one as many times as this value.
322      *
323      * @return threshold to determine whether 3D matched points are in a planar
324      * configuration.
325      */
326     public double getPlanarThreshold() {
327         return planarThreshold;
328     }
329 
330     /**
331      * Sets threshold to determine whether 3D matched points are in a planar
332      * configuration.
333      * Points are considered to be laying in a plane when the smallest singular
334      * value of their covariance matrix has a value much smaller than the
335      * largest one as many times as this value.
336      *
337      * @param planarThreshold threshold to determine whether 3D matched points
338      *                        are in a planar configuration.
339      * @throws IllegalArgumentException if provided threshold is negative.
340      * @throws LockedException          if estimator is locked.
341      */
342     public void setPlanarThreshold(final double planarThreshold) throws LockedException {
343         if (isLocked()) {
344             throw new LockedException();
345         }
346         if (planarThreshold < 0.0) {
347             throw new IllegalArgumentException();
348         }
349         this.planarThreshold = planarThreshold;
350     }
351 
352     /**
353      * Gets skewness value of intrinsic parameters to be used on estimated
354      * camera.
355      *
356      * @return skewness value of intrinsic parameters to be used on estimated
357      * camera.
358      */
359     public double getSkewness() {
360         return skewness;
361     }
362 
363     /**
364      * Sets skewness value of intrinsic parameters to be used on estimated
365      * camera.
366      *
367      * @param skewness skewness value of intrinsic parameters to be used on
368      *                 estimated camera.
369      * @throws LockedException if estimator is locked.
370      */
371     public void setSkewness(final double skewness) throws LockedException {
372         if (isLocked()) {
373             throw new LockedException();
374         }
375 
376         this.skewness = skewness;
377     }
378 
379     /**
380      * Returns horizontal coordinate of principal point on intrinsic parameters
381      * to be used on estimated camera.
382      *
383      * @return horizontal coordinate of principal point on intrinsic parameters
384      * to be used on estimated camera.
385      */
386     public double getHorizontalPrincipalPoint() {
387         return horizontalPrincipalPoint;
388     }
389 
390     /**
391      * Sets horizontal coordinate of principal point on intrinsic parameters to
392      * be used on estimated camera.
393      *
394      * @param horizontalPrincipalPoint horizontal coordinate of principal point
395      *                                 on intrinsic parameters to be used on estimated camera.
396      * @throws LockedException if estimator is locked.
397      */
398     public void setHorizontalPrincipalPoint(final double horizontalPrincipalPoint) throws LockedException {
399         if (isLocked()) {
400             throw new LockedException();
401         }
402 
403         this.horizontalPrincipalPoint = horizontalPrincipalPoint;
404     }
405 
406     /**
407      * Returns vertical coordinate of principal point on intrinsic parameters
408      * to be used on estimated camera.
409      *
410      * @return vertical coordinate of principal point on intrinsic parameters to
411      * be used on estimated camera.
412      */
413     public double getVerticalPrincipalPoint() {
414         return verticalPrincipalPoint;
415     }
416 
417     /**
418      * Sets vertical coordinate of principal point on intrinsic parameters
419      * to be used on estimated camera.
420      *
421      * @param verticalPrincipalPoint vertical coordinate of principal point on
422      *                               intrinsic parameters to be used on estimated camera.
423      * @throws LockedException if estimator is locked.
424      */
425     public void setVerticalPrincipalPoint(final double verticalPrincipalPoint) throws LockedException {
426         if (isLocked()) {
427             throw new LockedException();
428         }
429 
430         this.verticalPrincipalPoint = verticalPrincipalPoint;
431     }
432 
433     /**
434      * Indicates if this estimator is ready to start the estimation.
435      *
436      * @return true if estimator is ready, false otherwise.
437      */
438     @Override
439     public boolean isReady() {
440         return areListsAvailable() && areValidLists(points3D, points2D);
441     }
442 
443     /**
444      * Returns type of pinhole camera estimator.
445      *
446      * @return type of pinhole camera estimator.
447      */
448     @Override
449     public PinholeCameraEstimatorType getType() {
450         return PinholeCameraEstimatorType.UPNP_PINHOLE_CAMERA_ESTIMATOR;
451     }
452 
453     /**
454      * Indicates if provided point correspondences are normalized to increase
455      * the accuracy of the estimation.
456      *
457      * @return true if input point correspondences will be normalized, false
458      * otherwise.
459      */
460     @Override
461     public boolean arePointCorrespondencesNormalized() {
462         return false;
463     }
464 
465     /**
466      * Specifies whether provided point correspondences are normalized to
467      * increase the accuracy of the estimation.
468      *
469      * @param normalize true if input point correspondences will be normalized,
470      *                  false otherwise.
471      * @throws LockedException if estimator is locked.
472      */
473     @Override
474     public void setPointCorrespondencesNormalized(final boolean normalize) throws LockedException {
475 
476         if (isLocked()) {
477             throw new LockedException();
478         }
479     }
480 
481     /**
482      * Estimates a pinhole camera.
483      *
484      * @return estimated pinhole camera.
485      * @throws LockedException                 if estimator is locked.
486      * @throws NotReadyException               if input has not yet been provided.
487      * @throws PinholeCameraEstimatorException if an error occurs during
488      *                                         estimation, usually because input data is not valid.
489      */
490     @Override
491     public PinholeCamera estimate() throws LockedException, NotReadyException, PinholeCameraEstimatorException {
492         if (isLocked()) {
493             throw new LockedException();
494         }
495         if (!isReady()) {
496             throw new NotReadyException();
497         }
498 
499         try {
500             locked = true;
501             if (listener != null) {
502                 listener.onEstimateStart(this);
503             }
504 
505             computeWorldControlPointsAndPointConfiguration();
506             computeBarycentricCoordinates();
507             buildM();
508             solveNullspace();
509         } catch (final AlgebraException e) {
510             locked = false;
511             throw new PinholeCameraEstimatorException(e);
512         }
513 
514         solutions = new ArrayList<>();
515 
516         // general case
517         try {
518             generalSolution1();
519         } catch (final AlgebraException ignore) {
520             // if it fails, solution is not added
521         }
522         if (nullspaceDimension2Allowed) {
523             try {
524                 generalSolution2();
525             } catch (final AlgebraException ignore) {
526                 // if it fails, solution is not added
527             }
528         }
529 
530         // pick best solution
531         final var bestSolution = pickBestSolution();
532 
533         if (listener != null) {
534             listener.onEstimateEnd(this);
535         }
536 
537         if (bestSolution == null) {
538             throw new PinholeCameraEstimatorException();
539         }
540         locked = false;
541         return attemptRefine(bestSolution.camera);
542     }
543 
544     /**
545      * Indicates whether provided correspondences were found to be laying in a
546      * planar configuration during the estimation.
547      *
548      * @return true if point correspondences are in a planar configuration,
549      * false otherwise.
550      */
551     public boolean isPlanar() {
552         return isPlanar;
553     }
554 
555     /**
556      * Internal method that actually computes the normalized pinhole camera
557      * internal matrix.
558      * This implementation makes no action.
559      *
560      * @param points3D list of 3D points. Points might or might not be
561      *                 normalized.
562      * @param points2D list of 2D points. Points might or might not be
563      *                 normalized.
564      * @return matrix of estimated pinhole camera.
565      */
566     @Override
567     protected Matrix internalEstimate(final List<Point3D> points3D, final List<Point2D> points2D) {
568         return null;
569     }
570 
571     /**
572      * Internal method to set list of corresponding points (it does not check
573      * if estimator is locked).
574      *
575      * @param points3D list of corresponding 3D points.
576      * @param points2D list of corresponding 2D points.
577      * @throws IllegalArgumentException if any of the lists are null.
578      * @throws WrongListSizesException  if provided lists of points don't have
579      *                                  the same size and enough points.
580      */
581     private void internalSetListsUPnP(final List<Point3D> points3D, final List<Point2D> points2D)
582             throws WrongListSizesException {
583 
584         if (points3D == null || points2D == null) {
585             throw new IllegalArgumentException();
586         }
587 
588         if (!areValidLists(points3D, points2D)) {
589             throw new WrongListSizesException();
590         }
591 
592         this.points3D = points3D;
593         this.points2D = points2D;
594     }
595 
596     /**
597      * Picks best solution (the one having the smallest re-projection error).
598      *
599      * @return best solution.
600      */
601     private Solution pickBestSolution() {
602         Solution bestSolution = null;
603         var bestError = Double.MAX_VALUE;
604         for (final var s : solutions) {
605             if (s.reprojectionError < bestError) {
606                 bestError = s.reprojectionError;
607                 bestSolution = s;
608             }
609         }
610 
611         return bestSolution;
612     }
613 
614     /**
615      * Tests solution 2 for general point configuration.
616      * Because solution is up to scale, 4 different solutions for different
617      * beta1 and beta2 signs are tried.
618      *
619      * @throws AlgebraException if a numerical degeneracy occurs.
620      */
621     private void generalSolution2() throws AlgebraException {
622         if (isPlanar) {
623             return;
624         }
625 
626         final var va = nullspace.get(0);
627         final var vb = nullspace.get(1);
628 
629         final var controlCameraPointsA = controlPointsFromV(va);
630         final var controlCameraPointsB = controlPointsFromV(vb);
631 
632         final var c = constraintMatrixSolution2(controlCameraPointsA, controlCameraPointsB);
633         final var rhos = rhos(controlWorldPoints);
634 
635         final var a = Utils.solve(c, rhos);
636 
637         // a contains alpha1, alpha2, alpha3, alpha4, alpha5 and alpha6
638         // where:
639         // alpha1 = a[0] = beta1^2 --> beta11
640         // alpha2 = a[1] = beta1*beta2 --> beta12
641         // alpha3 = a[2] = beta2^2 --> beta22
642         // alpha4 = a[3] = beta1^2*f^2 --> betaff11
643         // alpha5 = a[4] = beta1*beta2*f^2 --> betaff12
644         // alpha6 = a[5] = beta2^2*f^2 --> betaff22
645 
646         // add solutions for the following triplets:
647         // [beta11, beta12, betaff11]
648         // [beta11, beta12, betaff12]
649         // [beta11, beta12, betaff22]
650         // [beta11, beta22, betaff11]
651         // the solution with the smallest re-projection error will be picked
652         double beta1;
653         double beta2;
654         double focalLength;
655         double initialBeta1;
656         double initialBeta2;
657 
658         // 1st triplet: [beta11, beta12, betaff11] = [alpha1, alpha2, alpha4]
659         // ------------------------------------------------------------------
660         initialBeta1 = beta1 = Math.sqrt(Math.abs(a[0]));
661         initialBeta2 = beta2 = a[1] / beta1;
662         focalLength = Math.sqrt(Math.abs(a[3] / a[0]));
663 
664         final var tmp1 = ArrayUtils.multiplyByScalarAndReturnNew(va, beta1);
665         final var tmp2 = ArrayUtils.multiplyByScalarAndReturnNew(vb, beta2);
666         final var finalV = ArrayUtils.sumAndReturnNew(tmp1, tmp2);
667         denormalizeV(finalV, focalLength);
668 
669         var controlCameraPoints = controlPointsFromV(finalV);
670 
671         Solution solution;
672         try {
673             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
674             solutions.add(solution);
675         } catch (final GeometryException ignore) {
676             // if it fails, solution is not added
677         }
678 
679         beta1 = -initialBeta1;
680         beta2 = -initialBeta2;
681 
682         ArrayUtils.multiplyByScalar(va, beta1, tmp1);
683         ArrayUtils.multiplyByScalar(vb, beta2, tmp2);
684         ArrayUtils.sum(tmp1, tmp2, finalV);
685         denormalizeV(finalV, focalLength);
686 
687         controlCameraPoints = controlPointsFromV(finalV);
688 
689         try {
690             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
691             solutions.add(solution);
692         } catch (final GeometryException ignore) {
693             // if it fails, solution is not added
694         }
695 
696         beta1 = initialBeta1;
697         beta2 = -initialBeta2;
698 
699         ArrayUtils.multiplyByScalar(va, beta1, tmp1);
700         ArrayUtils.multiplyByScalar(vb, beta2, tmp2);
701         ArrayUtils.sum(tmp1, tmp2, finalV);
702         denormalizeV(finalV, focalLength);
703 
704         controlCameraPoints = controlPointsFromV(finalV);
705 
706         try {
707             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
708             solutions.add(solution);
709         } catch (final GeometryException ignore) {
710             // if it fails, solution is not added
711         }
712 
713         beta1 = -initialBeta1;
714         beta2 = initialBeta2;
715 
716         ArrayUtils.multiplyByScalar(va, beta1, tmp1);
717         ArrayUtils.multiplyByScalar(vb, beta2, tmp2);
718         ArrayUtils.sum(tmp1, tmp2, finalV);
719         denormalizeV(finalV, focalLength);
720 
721         controlCameraPoints = controlPointsFromV(finalV);
722 
723         try {
724             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
725             solutions.add(solution);
726         } catch (final GeometryException ignore) {
727             // if it fails, solution is not added
728         }
729 
730         // 2nd triplet: [beta11, beta12, betaff12] = [alpha1, alpha2, alpha5]
731         // ------------------------------------------------------------------
732         initialBeta1 = beta1 = Math.sqrt(Math.abs(a[0]));
733         initialBeta2 = beta2 = a[1] / beta1;
734         focalLength = Math.sqrt(Math.abs(a[4] / a[1]));
735 
736         ArrayUtils.multiplyByScalar(va, beta1, tmp1);
737         ArrayUtils.multiplyByScalar(vb, beta2, tmp2);
738         ArrayUtils.sum(tmp1, tmp2, finalV);
739         denormalizeV(finalV, focalLength);
740 
741         controlCameraPoints = controlPointsFromV(finalV);
742 
743         try {
744             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
745             solutions.add(solution);
746         } catch (final GeometryException ignore) {
747             // if it fails, solution is not added
748         }
749 
750         beta1 = -initialBeta1;
751         beta2 = -initialBeta2;
752 
753         ArrayUtils.multiplyByScalar(va, beta1, tmp1);
754         ArrayUtils.multiplyByScalar(vb, beta2, tmp2);
755         ArrayUtils.sum(tmp1, tmp2, finalV);
756         denormalizeV(finalV, focalLength);
757 
758         controlCameraPoints = controlPointsFromV(finalV);
759 
760         try {
761             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
762             solutions.add(solution);
763         } catch (final GeometryException ignore) {
764             // if it fails, solution is not added
765         }
766 
767         beta1 = initialBeta1;
768         beta2 = -initialBeta2;
769 
770         ArrayUtils.multiplyByScalar(va, beta1, tmp1);
771         ArrayUtils.multiplyByScalar(vb, beta2, tmp2);
772         ArrayUtils.sum(tmp1, tmp2, finalV);
773         denormalizeV(finalV, focalLength);
774 
775         controlCameraPoints = controlPointsFromV(finalV);
776 
777         try {
778             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
779             solutions.add(solution);
780         } catch (final GeometryException ignore) {
781             // if it fails, solution is not added
782         }
783 
784         beta1 = -initialBeta1;
785         beta2 = initialBeta2;
786 
787         ArrayUtils.multiplyByScalar(va, beta1, tmp1);
788         ArrayUtils.multiplyByScalar(vb, beta2, tmp2);
789         ArrayUtils.sum(tmp1, tmp2, tmp1);
790         denormalizeV(finalV, focalLength);
791 
792         controlCameraPoints = controlPointsFromV(finalV);
793 
794         try {
795             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
796             solutions.add(solution);
797         } catch (final GeometryException ignore) {
798             //if it fails, solution is not added
799         }
800 
801         // 3rd triplet: [beta11, beta12, betaff22] = [alpha1, alpha2, alpha6]
802         // ------------------------------------------------------------------
803         initialBeta1 = beta1 = Math.sqrt(Math.abs(a[0]));
804         initialBeta2 = beta2 = a[1] / beta1;
805         focalLength = Math.sqrt(Math.abs(a[5] / a[2]));
806 
807         ArrayUtils.multiplyByScalar(va, beta1, tmp1);
808         ArrayUtils.multiplyByScalar(vb, beta2, tmp2);
809         ArrayUtils.sum(tmp1, tmp2, finalV);
810         denormalizeV(finalV, focalLength);
811 
812         controlCameraPoints = controlPointsFromV(finalV);
813 
814         try {
815             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
816             solutions.add(solution);
817         } catch (final GeometryException ignore) {
818             // if it fails, solution is not added
819         }
820 
821         beta1 = -initialBeta1;
822         beta2 = -initialBeta2;
823 
824         ArrayUtils.multiplyByScalar(va, beta1, tmp1);
825         ArrayUtils.multiplyByScalar(vb, beta2, tmp2);
826         ArrayUtils.sum(tmp1, tmp2, finalV);
827         denormalizeV(finalV, focalLength);
828 
829         controlCameraPoints = controlPointsFromV(finalV);
830 
831         try {
832             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
833             solutions.add(solution);
834         } catch (final GeometryException ignore) {
835             // if it fails, solution is not added
836         }
837 
838         beta1 = initialBeta1;
839         beta2 = -initialBeta2;
840 
841         ArrayUtils.multiplyByScalar(va, beta1, tmp1);
842         ArrayUtils.multiplyByScalar(vb, beta2, tmp2);
843         ArrayUtils.sum(tmp1, tmp2, finalV);
844         denormalizeV(finalV, focalLength);
845 
846         controlCameraPoints = controlPointsFromV(finalV);
847 
848         try {
849             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
850             solutions.add(solution);
851         } catch (final GeometryException ignore) {
852             // if it fails, solution is not added
853         }
854 
855         beta1 = -initialBeta1;
856         beta2 = initialBeta2;
857 
858         ArrayUtils.multiplyByScalar(va, beta1, tmp1);
859         ArrayUtils.multiplyByScalar(vb, beta2, tmp2);
860         ArrayUtils.sum(tmp1, tmp2, finalV);
861         denormalizeV(finalV, focalLength);
862 
863         controlCameraPoints = controlPointsFromV(finalV);
864 
865         try {
866             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
867             solutions.add(solution);
868         } catch (final GeometryException ignore) {
869             // if it fails, solution is not added
870         }
871 
872         // 4th triplet: [beta11, beta22, betaff11] = [alpha1, alpha3, alpha4]
873         // ------------------------------------------------------------------
874         initialBeta1 = beta1 = Math.sqrt(Math.abs(a[0]));
875         initialBeta2 = beta2 = Math.sqrt(Math.abs(a[2]));
876         focalLength = Math.sqrt(Math.abs(a[3] / a[0]));
877 
878         ArrayUtils.multiplyByScalar(va, beta1, tmp1);
879         ArrayUtils.multiplyByScalar(vb, beta2, tmp2);
880         ArrayUtils.sum(tmp1, tmp2, finalV);
881         denormalizeV(finalV, focalLength);
882 
883         controlCameraPoints = controlPointsFromV(finalV);
884 
885         try {
886             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
887             solutions.add(solution);
888         } catch (final GeometryException ignore) {
889             // if it fails, solution is not added
890         }
891 
892         beta1 = -initialBeta1;
893         beta2 = -initialBeta2;
894 
895         ArrayUtils.multiplyByScalar(va, beta1, tmp1);
896         ArrayUtils.multiplyByScalar(vb, beta2, tmp2);
897         ArrayUtils.sum(tmp1, tmp2, finalV);
898         denormalizeV(finalV, focalLength);
899 
900         controlCameraPoints = controlPointsFromV(finalV);
901 
902         try {
903             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
904             solutions.add(solution);
905         } catch (final GeometryException ignore) {
906             // if it fails, solution is not added
907         }
908 
909         beta1 = initialBeta1;
910         beta2 = -initialBeta2;
911 
912         ArrayUtils.multiplyByScalar(va, beta1, tmp1);
913         ArrayUtils.multiplyByScalar(vb, beta2, tmp2);
914         ArrayUtils.sum(tmp1, tmp2, finalV);
915         denormalizeV(finalV, focalLength);
916 
917         controlCameraPoints = controlPointsFromV(finalV);
918 
919         try {
920             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
921             solutions.add(solution);
922         } catch (final GeometryException ignore) {
923             // if it fails, solution is not added
924         }
925 
926         beta1 = -initialBeta1;
927         beta2 = initialBeta2;
928 
929         ArrayUtils.multiplyByScalar(va, beta1, tmp1);
930         ArrayUtils.multiplyByScalar(vb, beta2, tmp2);
931         ArrayUtils.sum(tmp1, tmp2, finalV);
932         denormalizeV(finalV, focalLength);
933 
934         controlCameraPoints = controlPointsFromV(finalV);
935 
936         try {
937             solution = computePossibleSolutionWithPoseAndReprojectionError(controlCameraPoints, focalLength);
938             solutions.add(solution);
939         } catch (final GeometryException ignore) {
940             // if it fails, solution is not added
941         }
942     }
943 
944     /**
945      * Fills constraint matrix to solve betas and focal length using control
946      * points (with normalized z coordinates by an unknown focal length) from
947      * last 2 columns of v (the null-space).
948      * The solution obtained with this constraint matrix and rhos will be
949      * control points in camera coordinates and estimated focal length.
950      *
951      * @param controlCameraPointsA control points of last column of v.
952      * @param controlCameraPointsB control points of second last column of v.
953      * @return constraint matrix to solve a linear system of equations.
954      * @throws AlgebraException never happens.
955      */
956     private static Matrix constraintMatrixSolution2(
957             final List<Point3D> controlCameraPointsA, final List<Point3D> controlCameraPointsB)
958             throws AlgebraException {
959 
960         final var numControl = controlCameraPointsA.size();
961         final var numEquations = numEquations(numControl);
962 
963         final var c = new Matrix(numEquations, 6);
964         var row = 0;
965         for (var i = 0; i < numControl; i++) {
966             final var vai = controlCameraPointsA.get(i);
967             final var vbi = controlCameraPointsB.get(i);
968 
969             for (var j = i + 1; j < numControl; j++) {
970                 final var vaj = controlCameraPointsA.get(j);
971                 final var vbj = controlCameraPointsB.get(j);
972 
973                 fillRowConstraintMatrixSolution2(row, c, vai, vaj, vbi, vbj);
974                 row++;
975             }
976         }
977 
978         return c;
979     }
980 
981     /**
982      * Fills a row of constraint matrix for solution2.
983      * Solution 2 takes into account the last 2 columns of v as its null-space:
984      * va = [vax, vay, vaz/f] and vb = [vbx, vby, vbz/f].
985      * Constraint:
986      * ||beta*vi - beta*vj||^2 = ||ci - cj||^2, i,j 1...4
987      * we need to find beta to scale control camera points, but since we are
988      * using 2 columns of the null-space v, then v is a linear combination
989      * v = beta1*vA + beta2*vB and the previous constraint becomes:
990      * ||(beta1*vAi + beta2*vBi) - (beta1*vAj + beta2*vBj)||^2 = ||ci - cj||^2, i,j 1...4
991      * This results in a linear system of 6 equations (when we have 4 control
992      * points) and 6 unknowns
993      * The previous constraint can be expanded as follows:
994      * ((beta1*vAi + beta2*vBi) - (beta1*vAj + beta2*vBj))^2 = (ci - cj)^2, i,j 1...4
995      * ((beta1*vAix + beta2*vBix) - (beta1*vAjx + beta2*vBjx))^2 + ((beta1*vAiy + beta2*vBiy) - (beta1*vAjy + beta2*vBjy))^2 + ((beta1*vAiz + beta2*vBiz)*f - (beta1*vAjz + beta2*vBjz)*f)^2 = (cix - cjx)^2 + (ciy - cjy)^2 + (ciz - cjz)^2,     i,j 1...4
996      * (beta1*(vAix - vAjx) + beta2*(vBix - vBjx))^2 + (beta1*(vAiy - vAjy) + beta2*(vBiy - vBjy))^2 + (beta1*(vAiz - vAjz)*f + beta2*(vBiz - vBjz)*f)^2 = (cix - cjx)^2 + (ciy - cjy)^2 + (ciz - cjz)^2,     i,j 1...4
997      * beta1^2*(vAix - vAjx)^2 + beta1*beta2*2*(vAix - vAjx)*(vBix - vBjx) + beta2^2*(vBix - vBjx)^2 + beta1^2*(vAiy - vAjy)^2 + beta1*beta2*2*(vAiy - vAjy)*(vBiy - vBjy) + beta2^2*(vBiy - vBjy)^2 + beta1^2*f^2*(vAiz - vAjz)^2 + beta1*beta2*f^2*2*(vAiz - vAjz)*(vBiz - vBjz) + beta2^2*f^2*(vBiz - vBjz)^2 = ((cix - cjx)^2 + (ciy - cjy)^2 + (ciz - cjz)^2),		i,j 1...4
998      * <p>
999      * Since beta1, beta2 and f are the unknowns, we can reorganize equation as:
1000      * beta1^2*((vAix - vAjx)^2 + (vAiy - vAjy)^2) +
1001      * beta1*beta2*2*((vAix - vAjx)*(vBix - vBjx) + (vAiy - vAjy)*(vBiy - vBjy)) +
1002      * beta2^2*((vBix - vBjx)^2 + (vBiy - vBjy)^2)+
1003      * beta1^2*f^2*(vAiz - vAjz)^2 +
1004      * beta1*beta2*f^2*2*(vAiz - vAjz)*(vBiz - vBjz) +
1005      * beta2^2*f^2*(vBiz - vBjz)^2 =
1006      * ((cix - cjx)^2 + (ciy - cjy)^2 + (ciz - cjz)^2),		i,j 1...4
1007      * <p>
1008      * The system is linearized assuming:
1009      * alpha1 = beta1^2
1010      * alpha2 = beta1*beta2
1011      * alpha3 = beta2^2
1012      * alpha4 = beta1^2*f^2
1013      * alpha5 = beta1*beta2*f^2
1014      * alpha6 = beta2^2*f^2
1015      * <p>
1016      * alpha1*((vAix - vAjx)^2 + (vAiy - vAjy)^2) +
1017      * alpha2*2*((vAix - vAjx)*(vBix - vBjx) + (vAiy - vAjy)*(vBiy - vBjy)) +
1018      * alpha3*((vBix - vBjx)^2 + (vBiy - vBjy)^2)+
1019      * alpha4*(vAiz - vAjz)^2 +
1020      * alpha5*2*(vAiz - vAjz)*(vBiz - vBjz) +
1021      * alpha6*(vBiz - vBjz)^2 =
1022      * ((cix - cjx)^2 + (ciy - cjy)^2 + (ciz - cjz)^2),		i,j 1...4
1023      *
1024      * @param row row to be filled.
1025      * @param c   matrix to be filled.
1026      * @param vai i-th control point in camera coordinates of last column of v
1027      *            (i.e. the nullspace) where z coordinate is normalized by some unknown
1028      *            focal length.
1029      * @param vaj j-th control point in camera coordinates of last column of v
1030      *            (i.e. the nullspace) where z coordinate is normalized by some unknown
1031      *            focal length.
1032      * @param vbi i-th control point in camera coordinates of second last column
1033      *            of v (i.e. the nullspace) where z coordinate is normalized by some
1034      *            unknown focal length.
1035      * @param vbj j-th control point in camera coordinates of second last column
1036      *            of v (i.e. the nullspace) where z coordinate is normalized by some
1037      *            unknown focal length.
1038      */
1039     private static void fillRowConstraintMatrixSolution2(
1040             final int row, final Matrix c, final Point3D vai, final Point3D vaj, final Point3D vbi, final Point3D vbj) {
1041 
1042         final var vaix = vai.getInhomX();
1043         final var vaiy = vai.getInhomY();
1044         final var vaiz = vai.getInhomZ();
1045 
1046         final var vajx = vaj.getInhomX();
1047         final var vajy = vaj.getInhomY();
1048         final var vajz = vaj.getInhomZ();
1049 
1050         final var vbix = vbi.getInhomX();
1051         final var vbiy = vbi.getInhomY();
1052         final var vbiz = vbi.getInhomZ();
1053 
1054         final var vbjx = vbj.getInhomX();
1055         final var vbjy = vbj.getInhomY();
1056         final var vbjz = vbj.getInhomZ();
1057 
1058         // 1st column
1059         c.setElementAt(row, 0, Math.pow(vaix - vajx, 2.0) + Math.pow(vaiy - vajy, 2.0));
1060 
1061         // 2nd column
1062         c.setElementAt(row, 1, 2.0 * ((vaix - vajx) * (vbix - vbjx) + (vaiy - vajy) * (vbiy - vbjy)));
1063 
1064         // 3rd column
1065         c.setElementAt(row, 2, Math.pow(vbix - vbjx, 2.0) + Math.pow(vbiy - vbjy, 2.0));
1066 
1067         // 4th column
1068         c.setElementAt(row, 3, Math.pow(vaiz - vajz, 2.0));
1069 
1070         // 5th column
1071         c.setElementAt(row, 4, 2.0 * (vaiz - vajz) * (vbiz - vbjz));
1072 
1073         // 6th column
1074         c.setElementAt(row, 5, Math.pow(vbiz - vbjz, 2.0));
1075     }
1076 
1077     /**
1078      * Tests solution 1 for general point configuration.
1079      * Because solution is up to scale. Two possible solutions must be evaluated
1080      * (positive or negative scale). The one with the smallest re-projection
1081      * error will be picked.
1082      *
1083      * @throws AlgebraException if a numerical degeneracy occurs.
1084      */
1085     private void generalSolution1() throws AlgebraException {
1086         // pick last column of null-space, contains control points in camera
1087         // coordinates up to scale (including sign change)
1088         final var v = nullspace.get(0);
1089 
1090         // The following constraint is imposed on the null-space of v = [vx, vy, vz/f].
1091         // Constraint:
1092         // ||beta*vi - beta*vj||^2 = ||ci - cj||^2, i,j 1...4
1093         // This results in a linear system of 6 equations (when we have 4 control points)
1094         // The previous constraint can be expanded as follows:
1095         // (beta*vi - beta*vj)^2 = (ci - cj)^2
1096         // (beta*vix - beta*vjx)^2 + (beta*viy - beta*vjy)^2 + (beta*viz*f - beta*vjz*f)^2 = (cix - cjx)^2 + (ciy - cjy)^2 + (ciz - cjz)^2,     i,j 1...4
1097         // beta^2*(vix - vjx)^2 + beta^2*(viy - vjy)^2 + beta^2*f^2*(viz - vjz)^2 = (cix - cjx)^2 + (ciy - cjy)^2 + (ciz - cjz)^2,      i,j 1...4
1098         // beta^2*((vix - vjx)^2 + (viy - vjy)^2) + beta^2*f^2*(viz - vjz)^2 = (cix - cjx)^2 + (ciy - cjy)^2 + (ciz - cjz)^2,       i,j 1...4
1099 
1100         // The system is linearized assuming
1101         // alpha1 = beta^2
1102         // alpha2 = beta^2*f^2
1103 
1104         // alpha1*((vix - vjx)^2 + (viy - vjy)^2) + alpha2*(viz - vjz)^2 = (cix - cjx)^2 + (ciy - cjy)^2 + (ciz - cjz)^2,       i,j 1...4
1105 
1106         final var controlCameraPoints = controlPointsFromV(v);
1107 
1108         final var c = constraintMatrixSolution1(controlCameraPoints);
1109         final var rhos = rhos(controlWorldPoints);
1110 
1111         final var a = Utils.solve(c, rhos);
1112 
1113         // a contains alpha1 and alpha2
1114 
1115         // sign can change
1116         final var beta = Math.sqrt(Math.abs(a[0]));
1117         // always positive
1118         final var focalLength = Math.sqrt(Math.abs(a[1] / a[0]));
1119 
1120         // apply beta scale and denormalize using estimated focal length
1121         final var finalV = ArrayUtils.multiplyByScalarAndReturnNew(v, beta);
1122         denormalizeV(finalV, focalLength);
1123 
1124         var finalControlCameraPoints = controlPointsFromV(finalV);
1125 
1126         Solution solution;
1127         try {
1128             solution = computePossibleSolutionWithPoseAndReprojectionError(finalControlCameraPoints, focalLength);
1129             solutions.add(solution);
1130         } catch (final GeometryException ignore) {
1131             // if it fails, solution is not added
1132         }
1133 
1134         // add solution with opposite beta sign
1135         ArrayUtils.multiplyByScalar(v, -beta, finalV);
1136         denormalizeV(finalV, focalLength);
1137 
1138         finalControlCameraPoints = controlPointsFromV(finalV);
1139 
1140         try {
1141             solution = computePossibleSolutionWithPoseAndReprojectionError(finalControlCameraPoints, focalLength);
1142             solutions.add(solution);
1143         } catch (final GeometryException ignore) {
1144             // if it fails, solution is not added
1145         }
1146     }
1147 
1148     /**
1149      * Denormalizes v array containing the null-space of M, which contains the
1150      * control points in camera coordinates in consecutive order but having z
1151      * coordinates normalized by focal length.
1152      * After execution of this method, z coordinates will be denormalized.
1153      *
1154      * @param v           array containing the null-space of M with normalized z
1155      *                    coordinates.
1156      * @param focalLength focal length to use for de-normalization.
1157      */
1158     private static void denormalizeV(final double[] v, final double focalLength) {
1159         for (int i = 0, j = 1; i < v.length; i++, j++) {
1160             if (j % Point3D.POINT3D_INHOMOGENEOUS_COORDINATES_LENGTH == 0) {
1161                 v[i] *= focalLength;
1162             }
1163         }
1164     }
1165 
1166     /**
1167      * Fills constraint matrix to solve beta and focal length using control
1168      * points (with normalized z coordinates by an unknown focal length) from
1169      * the last column of v (the null-space).
1170      * The solution obtained with this constraint matrix and rhos will be
1171      * control points in camera coordinates and estimated focal length.
1172      *
1173      * @param controlCameraPoints control points of last column of v.
1174      * @return constraint matrix to solve a linear system of equations.
1175      * @throws AlgebraException never happens.
1176      */
1177     private static Matrix constraintMatrixSolution1(final List<Point3D> controlCameraPoints) throws AlgebraException {
1178 
1179         final var numControl = controlCameraPoints.size();
1180         final var numEquations = numEquations(numControl);
1181 
1182         final var c = new Matrix(numEquations, 2);
1183         var row = 0;
1184         for (var i = 0; i < numControl; i++) {
1185             final var vi = controlCameraPoints.get(i);
1186 
1187             for (var j = i + 1; j < numControl; j++) {
1188                 final var vj = controlCameraPoints.get(j);
1189 
1190                 fillRowConstraintMatrixSolution1(row, c, vi, vj);
1191                 row++;
1192             }
1193         }
1194 
1195         return c;
1196     }
1197 
1198     /**
1199      * Fills a row of constraint matrix for solution 1.
1200      * The following constraint is imposed on the null-space of
1201      * v = [vx, vy, vz/f].
1202      * Constraint:
1203      * ||beta*vi - beta*vj||^2 = ||ci - cj||^2, i,j 1...4
1204      * This results in a linear system of 6 equations (when we have 4 control
1205      * points) and 2 unknowns.
1206      * The previous constraint can be expanded as follows:
1207      * (beta*vi - beta*vj)^2 = (ci - cj)^2
1208      * (beta*vix - beta*vjx)^2 + (beta*viy - beta*vjy)^2 + (beta*viz*f - beta*vjz*f)^2 = (cix - cjx)^2 + (ciy - cjy)^2 + (ciz - cjz)^2,     i,j 1...4
1209      * beta^2*(vix - vjx)^2 + beta^2*(viy - vjy)^2 + beta^2*f^2*(viz - vjz)^2 = (cix - cjx)^2 + (ciy - cjy)^2 + (ciz - cjz)^2,      i,j 1...4
1210      * beta^2*((vix - vjx)^2 + (viy - vjy)^2) + beta^2*f^2*(viz - vjz)^2 = (cix - cjx)^2 + (ciy - cjy)^2 + (ciz - cjz)^2,       i,j 1...4
1211      * <p>
1212      * The system is linearized assuming
1213      * alpha1 = beta^2
1214      * alpha2 = beta^2/f^2
1215      * <p>
1216      * alpha1*((vix - vjx)^2 + (viy - vjy)^2) + alpha2*(viz - vjz)^2 = (cix - cjx)^2 + (ciy - cjy)^2 + (ciz - cjz)^2,       i,j 1...4
1217      *
1218      * @param row row to be filled.
1219      * @param c   matrix to be filled.
1220      * @param vi  i-th control point in camera coordinates of last column of v
1221      *            (i.e. the null-space of m) where z coordinate is normalized by some
1222      *            unknown focal length.
1223      * @param vj  j-th control point in camera coordinates of last column of v
1224      *            (i.e. the nullspace of m) where z coordinate is normalized by some
1225      *            unknown focal length.
1226      */
1227     private static void fillRowConstraintMatrixSolution1(
1228             final int row, final Matrix c, final Point3D vi, final Point3D vj) {
1229         final var vix = vi.getInhomX();
1230         final var viy = vi.getInhomY();
1231         // normalized by unknown focal length
1232         final var viz = vi.getInhomZ();
1233 
1234         final var vjx = vj.getInhomX();
1235         final var vjy = vj.getInhomY();
1236         // normalized by unknown focal length
1237         final var vjz = vj.getInhomZ();
1238 
1239         // 1st column
1240         c.setElementAt(row, 0, Math.pow(vix - vjx, 2.0) + Math.pow(viy - vjy, 2.0));
1241 
1242         // 2nd column
1243         c.setElementAt(row, 1, Math.pow(viz - vjz, 2.0));
1244     }
1245 
1246     /**
1247      * Computes a possible solution with camera, transformation, re-projection
1248      * error and control points in camera coordinates.
1249      *
1250      * @param controlCameraPoints control points in camera coordinates.
1251      * @param focalLength         estimated focal length.
1252      * @return a possible solution.
1253      * @throws LockedException           never happens.
1254      * @throws NotReadyException         never happens.
1255      * @throws CoincidentPointsException if a point degeneracy has occurred.
1256      */
1257     private Solution computePossibleSolutionWithPoseAndReprojectionError(
1258             final List<Point3D> controlCameraPoints, final double focalLength) throws LockedException,
1259             NotReadyException, CoincidentPointsException {
1260 
1261         final var worldToCameraTransformation = worldToCameraTransformationMetric(controlCameraPoints);
1262 
1263         final var rotation = worldToCameraTransformation.getRotation();
1264         final var t = worldToCameraTransformation.getTranslation();
1265         final var scale = worldToCameraTransformation.getScale();
1266 
1267         // Camera center is C = -1/s*R'*t
1268         final var center = new InhomogeneousPoint3D(-t[0] / scale, -t[1] / scale, -t[2] / scale);
1269         final var invRotation = rotation.inverseRotationAndReturnNew();
1270         invRotation.rotate(center, center);
1271 
1272         final var intrinsic = new PinholeCameraIntrinsicParameters(focalLength, focalLength, horizontalPrincipalPoint,
1273                 verticalPrincipalPoint, skewness);
1274 
1275         final var camera = new PinholeCamera(intrinsic, rotation, center);
1276 
1277         final var solution = new Solution();
1278         solution.controlCameraPoints = controlCameraPoints;
1279         solution.worldToCameraTransformation = worldToCameraTransformation;
1280         solution.camera = camera;
1281 
1282         // compute projection error
1283         solution.reprojectionError = reprojectionError(camera);
1284 
1285         return solution;
1286     }
1287 
1288     /**
1289      * Estimates world to camera transformation using estimated control points
1290      * in world and camera coordinates as a metric transformation.
1291      *
1292      * @param controlCameraPoints control points in camera coordinates.
1293      * @return metric transformation relating control points from world to
1294      * camera coordinates.
1295      * @throws LockedException           never happens.
1296      * @throws NotReadyException         never happens.
1297      * @throws CoincidentPointsException if a point degeneracy has occurred.
1298      */
1299     private MetricTransformation3D worldToCameraTransformationMetric(final List<Point3D> controlCameraPoints)
1300             throws LockedException, NotReadyException, CoincidentPointsException {
1301         final var estimator = new MetricTransformation3DEstimator(controlWorldPoints, controlCameraPoints, isPlanar);
1302         return estimator.estimate();
1303     }
1304 
1305     /**
1306      * Number of equations required to solve constraints for case 1 to 4.
1307      *
1308      * @param numControl number of control points.
1309      * @return number of constraint equations.
1310      */
1311     private static int numEquations(final int numControl) {
1312         var numEquations = 0;
1313         for (var i = 1; i < numControl; i++) {
1314             numEquations += i;
1315         }
1316         return numEquations;
1317     }
1318 
1319     /**
1320      * Right term of linearized system of equations to solve betas.
1321      *
1322      * @param controlWorldPoints control points in world coordinates.
1323      * @return right term.
1324      */
1325     private static double[] rhos(final List<Point3D> controlWorldPoints) {
1326         final var numControl = controlWorldPoints.size();
1327         final var numEquations = numEquations(numControl);
1328         final var rhos = new double[numEquations];
1329 
1330         // squared distance from control world i to control world j
1331         var pos = 0;
1332         for (var i = 0; i < numControl; i++) {
1333             final var ci = controlWorldPoints.get(i);
1334 
1335             for (var j = i + 1; j < numControl; j++) {
1336                 final var cj = controlWorldPoints.get(j);
1337 
1338                 final var dcijSqr = Math.pow(ci.distanceTo(cj), 2.0);
1339                 rhos[pos] = dcijSqr;
1340                 pos++;
1341             }
1342         }
1343 
1344         return rhos;
1345     }
1346 
1347     /**
1348      * Total re-projection error for provided camera.
1349      *
1350      * @param camera camera to estimate re-projection error.
1351      * @return re-projection error.
1352      */
1353     private double reprojectionError(final PinholeCamera camera) {
1354         final var n = points2D.size();
1355 
1356         final var projected = Point2D.create();
1357         var error = 0.0;
1358         for (int i = 0; i < n; i++) {
1359             final var point3D = points3D.get(i);
1360             final var point2D = points2D.get(i);
1361             camera.project(point3D, projected);
1362             error += projected.distanceTo(point2D);
1363         }
1364         return error;
1365     }
1366 
1367     /**
1368      * Computes list of control points from provided array containing one column
1369      * of the null-space of M or a linear combination of columns of the
1370      * null-space.
1371      *
1372      * @param v one column of the null-space of M or a linear combination of
1373      *          columns of the null-space.
1374      * @return control points.
1375      */
1376     private List<Point3D> controlPointsFromV(final double[] v) {
1377         final var numControl = controlWorldPoints.size();
1378         final var points = new ArrayList<Point3D>();
1379 
1380         for (var j = 0; j < numControl; j++) {
1381             final var k = j * 3;
1382             final var p = new InhomogeneousPoint3D(v[k], v[k + 1], v[k + 2]);
1383             points.add(p);
1384         }
1385 
1386         return points;
1387     }
1388 
1389     /**
1390      * Solves null-space of matrix M containing possible solutions of camera
1391      * coordinates of control points.
1392      *
1393      * @throws AlgebraException if something fails due to numerical
1394      *                          instabilities.
1395      */
1396     private void solveNullspace() throws AlgebraException {
1397         final var rows = m.getRows();
1398         final var cols = m.getColumns();
1399         final var numControl = cols / Point3D.POINT3D_INHOMOGENEOUS_COORDINATES_LENGTH;
1400 
1401         // normalize rows of m to increase numerical accuracy
1402         for (var i = 0; i < rows; i++) {
1403             normalizeRow(m, i);
1404         }
1405 
1406         final var decomposer = new SingularValueDecomposer(m);
1407         decomposer.decompose();
1408 
1409         // Singular values are always in descending order, hence null space is in
1410         // the last columns of v.
1411         // V is 12x12 (general configuration) or 9x9 (planar configuration).
1412         // Each column of v contains coordinates of control points in camera
1413         // coordinates.
1414         // A solution for the linear system M*x = 0 is obtained as a linear
1415         // combination of the columns of v forming the null-space.
1416         final var v = decomposer.getV();
1417 
1418         // although nullity of M could be determined after SVD, it is assumed
1419         // instead that null-space could be located in any of the latter columns
1420         // of v up to the number of control points.
1421         // Hence, for general configuration we pick the last 4 columns of v and
1422         // for planar configuration we pick the last 3.
1423 
1424         // extract null points from the null space
1425         nullspace = new ArrayList<>();
1426         final var colsMinusOne = cols - 1;
1427         for (int i = 0; i < numControl; i++) {
1428             final var column = colsMinusOne - i;
1429 
1430             // each picked column of v contains a possible solution
1431             final var vCol = v.getSubmatrixAsArray(0, column, colsMinusOne, column);
1432             nullspace.add(vCol);
1433         }
1434     }
1435 
1436     /**
1437      * Normalizes provided row of m.
1438      *
1439      * @param m   matrix to be normalized.
1440      * @param row row to be normalized.
1441      */
1442     private static void normalizeRow(final Matrix m, final int row) {
1443         final var cols = m.getColumns();
1444 
1445         var norm = 0.0;
1446         for (var i = 0; i < cols; i++) {
1447             norm += Math.pow(m.getElementAt(row, i), 2.0);
1448         }
1449         norm = Math.sqrt(norm);
1450 
1451         for (var i = 0; i < cols; i++) {
1452             m.setElementAt(row, i, m.getElementAt(row, i) / norm);
1453         }
1454     }
1455 
1456     /**
1457      * In order to find control points in camera coordinates, an homogeneous
1458      * linear system of equations must be solved having the form M*x = 0, where
1459      * x contains the coordinates of all control points in the form [x1, y1,
1460      * z1/f, x2, y2, z2/f, ... ] where f is an unknown focal length normalizing
1461      * z terms.
1462      * For general configuration there are 4 control points, hence x has length
1463      * 12 (3 coordinates * 4 control points).
1464      * For a planar configuration there are 3 control points, hence x has length
1465      * 9 (3 coordinates * 3 control points).
1466      * This method builds M matrix required to solve such linear system of
1467      * equations, where M has size 2*n x 12 (general configuration) or 2*n x 9
1468      * (planar configuration), where n is the number of provided 2D observed
1469      * points.
1470      *
1471      * @throws AlgebraException if numerical instabilities occur.
1472      */
1473     private void buildM() throws AlgebraException {
1474         final var n = points2D.size();
1475         final var numControlPoints = alphas.getColumns();
1476 
1477         m = new Matrix(2 * n, 3 * numControlPoints);
1478 
1479         for (var i = 0; i < n; i++) {
1480             final var p = points2D.get(i);
1481             final var pX = p.getInhomX();
1482             final var pY = p.getInhomY();
1483 
1484             final var row = i * 2;
1485 
1486             for (var j = 0; j < numControlPoints; j++) {
1487                 final var col = j * 3;
1488 
1489                 final var alpha = alphas.getElementAt(i, j);
1490 
1491                 m.setElementAt(row, col, alpha);
1492                 m.setElementAt(row, col + 1, alpha * skewness);
1493                 m.setElementAt(row, col + 2, alpha * (horizontalPrincipalPoint - pX));
1494 
1495                 m.setElementAt(row + 1, col, 0.0);
1496                 m.setElementAt(row + 1, col + 1, alpha);
1497                 m.setElementAt(row + 1, col + 2, alpha * (verticalPrincipalPoint - pY));
1498             }
1499         }
1500     }
1501 
1502     /**
1503      * Computes the coordinates of each provided world point in terms of
1504      * estimated control points in world coordinates.
1505      * Such coordinates (i.e. barycentric coordinates) are stored in alphas
1506      * matrix, where each row contains the coordinates of each world point in
1507      * terms of control points.
1508      * For general configuration, each row contains 4 coordinates and alphas
1509      * has size nx4, where n is the number of provided 3D world points.
1510      * For planar configuration, each row contains 3 coordinates and alphas
1511      * has size nx3, where n is the number of provided 3D world points.
1512      * Because world and camera coordinates are related by a rotation (since
1513      * both reference frames are centered in the centroid), alphas can be used
1514      * in both world and camera coordinates.
1515      *
1516      * @throws AlgebraException if there are numerical instabilities.
1517      */
1518     private void computeBarycentricCoordinates() throws AlgebraException {
1519         // we need to express world points in terms of control points in world
1520         // coordinates
1521 
1522         // In the general configuration case:
1523         // For a point p1 in world inhomogeneous coordinates
1524         // p1 =  alpha1 + c1 + alpha2 * c2 + alpha3 * c3 + alpha4 * c4
1525         // where alpha1, alpha2, alpha3, alpha4 are scalars and
1526         // c1, c2, c3 are the control points in the principal axes and
1527         // centroid is the last control point c4, all 4 expressed in world
1528         // inhomogeneous coordinates as 3-column vectors.
1529 
1530         // Assuming a matrix form:
1531         // [p1] = [c1 c2 c3 c4]*[alpha1]
1532         //                      [alpha2]
1533         //                      [alpha3]
1534         //                      [alpha4]
1535 
1536         // or in simpler for p = C * alpha, where p is a 3-column vector, C is a
1537         // 3x4 matrix and alpha is a 4-1 vector.
1538         // This can be repeated for each i-th point so that:
1539         // pi = C * alphai --> alphai = inv(C)*pi
1540         // However, in this form C is not invertible because it is rank deficient
1541         // To avoid this deficiency we add the constraint that the sum of alphas
1542         // for a point must be 1, so we can use the reduced form:
1543         // [p1 - c4] = [(c1 - c4) (c2 - c4) (c3 - c4)]*[alpha1]
1544         //                                             [alpha2]
1545         //                                             [alpha3]
1546         // and set alpha4 = 1 - alpha1 - alpha2 - alpha3
1547 
1548         // This way the equation still holds:
1549         // p1 - c4 = (c1 - c4) * alpha1 + (c2 - c4) * alpha2 + (c3 - c4) * alpha3 =
1550         //         = c1 * alpha1 + c2 * alpha2 + c3 * alpha3 - c4 * (alpha1 + alpha2 + alpha3)
1551         // p1 = c1 * alpha1 + c2 * alpha2 * c3 * alpha3 + c4 * (1 - alpha1 - alpha2 - alpha3)
1552 
1553         // This way, we create reduced matrix C as having 3 rows (one for each
1554         // inhomogeneous coordinate) and 3 columns in the general case.
1555 
1556         // In the planar case we have only 3 control points, and the last one
1557         // (c3) is the centroid.
1558 
1559         final var numControl = controlWorldPoints.size();
1560         final var numDimensions = numControl - 1;
1561         final var numControlMinusTwo = numControl - 2;
1562         final var c = new Matrix(Point3D.POINT3D_INHOMOGENEOUS_COORDINATES_LENGTH, numDimensions);
1563 
1564         // the last control point is the centroid (or mean point)
1565         final var mean = controlWorldPoints.get(numDimensions);
1566         final var meanX = mean.getInhomX();
1567         final var meanY = mean.getInhomY();
1568         final var meanZ = mean.getInhomZ();
1569 
1570         for (var i = 0; i < numDimensions; i++) {
1571             final var controlPoint = controlWorldPoints.get(i);
1572             c.setElementAt(0, i, controlPoint.getInhomX() - meanX);
1573             c.setElementAt(1, i, controlPoint.getInhomY() - meanY);
1574             c.setElementAt(2, i, controlPoint.getInhomZ() - meanZ);
1575         }
1576 
1577         // to find  reduced alphas, we need to inverse the reduced C matrix and
1578         // multiply it by [p - centroid], where centroid can be c4 or c3 in
1579         // planar case.
1580 
1581         final var invC = Utils.inverse(c);
1582 
1583         // x is p - centroid, where p is each 3D world point
1584         final var n = points3D.size();
1585         final var reducedPoint = new Matrix(Point3D.POINT3D_INHOMOGENEOUS_COORDINATES_LENGTH, 1);
1586         final var reducedAlpha = new Matrix(numDimensions, 1);
1587         alphas = new Matrix(n, numControl);
1588         for (var i = 0; i < n; i++) {
1589             final var worldPoint = points3D.get(i);
1590             reducedPoint.setElementAtIndex(0, worldPoint.getInhomX() - meanX);
1591             reducedPoint.setElementAtIndex(1, worldPoint.getInhomY() - meanY);
1592             reducedPoint.setElementAtIndex(2, worldPoint.getInhomZ() - meanZ);
1593 
1594             invC.multiply(reducedPoint, reducedAlpha);
1595             final var buffer = reducedAlpha.getBuffer();
1596 
1597             // copy reducedAlpha into the former components of i-th row of alphas
1598             alphas.setSubmatrix(i, 0, i, numControlMinusTwo, buffer);
1599 
1600             // The last component of each alpha for each point is computed so
1601             // that their sum is equal to one
1602             if (numControl == GENERAL_NUM_CONTROL_POINTS) {
1603                 // general configuration
1604                 alphas.setElementAt(i, numDimensions, 1.0 - buffer[0] - buffer[1] - buffer[2]);
1605             } else {
1606                 // planar configuration
1607                 alphas.setElementAt(i, numDimensions, 1.0 - buffer[0] - buffer[1]);
1608             }
1609         }
1610     }
1611 
1612     /**
1613      * Computes control points in world coordinates and determines whether
1614      * they are located in a planar configuration or not.
1615      * This method computes the centroid of provided 3D points and their
1616      * covariance.
1617      * Uses PCA by means of SVD decomposition of their covariance matrix in
1618      * order to find the principal directions of the cloud formed by the
1619      * collection of points and sets control points as the computed centroid
1620      * and points along the principal axes so that they form a basis that
1621      * can be used to express any 3D points into.
1622      * If the smallest singular value is close to zero in comparison to the
1623      * largest one, then it is assumed that 3D points are in a planar
1624      * configuration.
1625      * If a planar configuration is allowed, then only 3 control points are
1626      * computed along the plane using the centroid and two points on the
1627      * principal directions of such plane.
1628      * Otherwise, in general configuration, 4 control points are computed as
1629      * the centroid and 3 points along the principal axes of the cloud of 3D
1630      * points.
1631      *
1632      * @throws AlgebraException if something fails because of numerical
1633      *                          instabilities.
1634      */
1635     private void computeWorldControlPointsAndPointConfiguration() throws AlgebraException {
1636         final var centroid = Point3D.centroid(points3D);
1637 
1638         // covariance matrix elements, summed up here for speed
1639         var c11 = 0.0;
1640         var c12 = 0.0;
1641         var c13 = 0.0;
1642         var c22 = 0.0;
1643         var c23 = 0.0;
1644         var c33 = 0.0;
1645         final var n = points3D.size();
1646         for (final var point : points3D) {
1647             final var dx = point.getInhomX() - centroid.getInhomX();
1648             final var dy = point.getInhomY() - centroid.getInhomY();
1649             final var dz = point.getInhomZ() - centroid.getInhomZ();
1650 
1651             c11 += dx * dx;
1652             c12 += dx * dy;
1653             c13 += dx * dz;
1654 
1655             c22 += dy * dy;
1656             c23 += dy * dz;
1657 
1658             c33 += dz * dz;
1659         }
1660         c11 /= n;
1661         c12 /= n;
1662         c13 /= n;
1663         c22 /= n;
1664         c23 /= n;
1665         c33 /= n;
1666 
1667         final var covar = new Matrix(3, 3);
1668         covar.setElementAt(0, 0, c11);
1669         covar.setElementAt(1, 0, c12);
1670         covar.setElementAt(2, 0, c13);
1671 
1672         covar.setElementAt(0, 1, c12);
1673         covar.setElementAt(1, 1, c22);
1674         covar.setElementAt(2, 1, c23);
1675 
1676         covar.setElementAt(0, 2, c13);
1677         covar.setElementAt(1, 2, c23);
1678         covar.setElementAt(2, 2, c33);
1679 
1680         final var decomposer = new SingularValueDecomposer(covar);
1681         decomposer.decompose();
1682 
1683         final var singularValues = decomposer.getSingularValues();
1684         final var v = decomposer.getV();
1685 
1686         // planar check
1687         int numControl;
1688         if (!planarConfigurationAllowed
1689                 || Math.abs(singularValues[0]) < Math.abs(singularValues[2]) * planarThreshold) {
1690             // general configuration
1691             numControl = GENERAL_NUM_CONTROL_POINTS;
1692             isPlanar = false;
1693         } else {
1694             // planar configuration (only if allowed)
1695             numControl = PLANAR_NUM_CONTROL_POINTS;
1696             isPlanar = true;
1697         }
1698 
1699         controlWorldPoints = new ArrayList<>();
1700 
1701         final var centroidX = centroid.getInhomX();
1702         final var centroidY = centroid.getInhomY();
1703         final var centroidZ = centroid.getInhomZ();
1704 
1705         final var numDimensions = numControl - 1;
1706         final var k = Math.sqrt(singularValues[0] / n);
1707         for (var i = 0; i < numDimensions; i++) {
1708             final var vx = v.getElementAt(0, i) * k;
1709             final var vy = v.getElementAt(1, i) * k;
1710             final var vz = v.getElementAt(2, i) * k;
1711 
1712             controlWorldPoints.add(new InhomogeneousPoint3D(centroidX + vx, centroidY + vy, centroidZ + vz));
1713         }
1714 
1715         // add centroid (it will be used for the metric transformation
1716         // estimation)
1717         controlWorldPoints.add(centroid);
1718     }
1719 
1720     /**
1721      * A possible solution.
1722      */
1723     private static class Solution {
1724         /**
1725          * Control points in camera coordinates.
1726          */
1727         List<Point3D> controlCameraPoints;
1728 
1729         /**
1730          * Transformation from world to camera coordinates.
1731          * Point projection is expressed by x = P * Xw, where P is a pinhole
1732          * camera and Xw is a point in world coordinates.
1733          * Points in camera coordinates are expressed as:
1734          * Xc = Tw-&lt;c * Xw, where Tw-&lt;c is the transformation from world to
1735          * camera.
1736          * The Euclidean transformation Tw-&lt;c is expressed as:
1737          * Tw-&lt;c = [R  t]
1738          * [0' 1]
1739          * Projection of a point in camera coordinates can also be expressed
1740          * as x = Pc * Xc = K * [I 0] * Xc
1741          * where Pc is a camera and has the form Pc = K *[I 0], so that
1742          * x = Pc * Xc = K * [I 0] * Xc = K * [I 0] * Tw-&lt;c * Xw
1743          * x = K * [I 0] * [R  t] * Xw = K * [I*R + 0, I*t + 0] * Xw =
1744          * [0' 1]
1745          * x = K * [R t] * Xw = K * [R - R*C] * Xw = x = P * Xw,
1746          * where R is a rotation and C is the camera center in world
1747          * coordinates.
1748          * Assuming that control points are obtained up to scale, then instead
1749          * of an Euclidean transformation we will assume that Tw-&lt;c is a metric
1750          * transformation, hence:
1751          * Tw-&lt;c = [s*R t2]
1752          * [0'  1 ]
1753          * To obtain the previous equation, then point in camera coordinates
1754          * must be 1/s*Xc so that:
1755          * x = Pc * 1/s * Xc = K * [I 0] * 1/s * Xc
1756          * x = K * [I 0] * 1/s * Tw-&lt;c * Xw
1757          * x = K * [I 0] * 1/s *[s*R t2] * Xw = K * 1/s * [I*s*R + 0, I*t2 + 0]
1758          * [0'  1 ]
1759          * x = K * 1 / s * [s*R t2] * Xw = K * [R 1/s*t2] * Xw
1760          * where t = 1/s*t2 = -R*C and so again
1761          * x = K * [R t] * Xw
1762          * and camera center is C = -1/s*R'*t2
1763          */
1764         MetricTransformation3D worldToCameraTransformation;
1765 
1766         /**
1767          * Pinhole camera using provided intrinsic parameters and estimated
1768          * transformation for this solution.
1769          */
1770         PinholeCamera camera;
1771 
1772         /**
1773          * Re-projection error.
1774          */
1775         double reprojectionError;
1776     }
1777 }