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-<c * Xw, where Tw-<c is the transformation from world to
1735 * camera.
1736 * The Euclidean transformation Tw-<c is expressed as:
1737 * Tw-<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-<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-<c is a metric
1750 * transformation, hence:
1751 * Tw-<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-<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 }