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.ar.sfm;
17  
18  import com.irurueta.algebra.AlgebraException;
19  import com.irurueta.ar.slam.BaseCalibrationData;
20  import com.irurueta.ar.slam.BaseSlamEstimator;
21  import com.irurueta.geometry.GeometryException;
22  import com.irurueta.geometry.InhomogeneousPoint3D;
23  import com.irurueta.geometry.MetricTransformation3D;
24  import com.irurueta.geometry.PinholeCamera;
25  import com.irurueta.geometry.PinholeCameraIntrinsicParameters;
26  import com.irurueta.geometry.Quaternion;
27  import com.irurueta.geometry.Rotation3D;
28  
29  import java.util.ArrayList;
30  
31  @SuppressWarnings("DuplicatedCode")
32  public abstract class BaseSlamPairedViewsSparseReconstructor<
33          D extends BaseCalibrationData,
34          C extends BaseSlamPairedViewsSparseReconstructorConfiguration<D, C>,
35          R extends BaseSlamPairedViewsSparseReconstructor<D, C, R, L, S>,
36          L extends BaseSlamPairedViewsSparseReconstructorListener<R>,
37          S extends BaseSlamEstimator<D>> extends BasePairedViewsSparseReconstructor<C, R, L> {
38  
39      /**
40       * Slam estimator to estimate position, speed, orientation using
41       * accelerometer and gyroscope data.
42       */
43      protected S slamEstimator;
44  
45      /**
46       * Position estimated by means of SLAM. It is reused each time it is notified.
47       */
48      private final InhomogeneousPoint3D slamPosition = new InhomogeneousPoint3D();
49  
50      /**
51       * Inverse Euclidean camera rotation. This is reused for memory efficiency.
52       */
53      private Rotation3D invEuclideanCameraRotation;
54  
55      /**
56       * Camera estimated by means of SLAM. It is reused each time it is notified.
57       */
58      private final PinholeCamera slamCamera = new PinholeCamera();
59  
60      /**
61       * Camera rotation estimated by means of SLAM. It is reused each time it is notified.
62       */
63      private final Quaternion slamRotation = new Quaternion();
64  
65      /**
66       * Last SLAM timestamp.
67       */
68      private long lastTimestamp = -1;
69  
70      /**
71       * Last view pair SLAM timestamp.
72       */
73      private long lastViewPairTimestamp = -1;
74  
75      /**
76       * Constructor.
77       *
78       * @param configuration configuration for this re-constructor.
79       * @param listener      listener in charge of handling events.
80       * @throws NullPointerException if listener or configuration is not
81       *                              provided.
82       */
83      protected BaseSlamPairedViewsSparseReconstructor(final C configuration, final L listener) {
84          super(configuration, listener);
85      }
86  
87      /**
88       * Provides a new accelerometer sample to update SLAM estimation.
89       * This method must be called whenever the accelerometer sensor receives new
90       * data.
91       * If re-constructor is not running, calling this method has no effect.
92       *
93       * @param timestamp     timestamp of accelerometer sample since epoch time and
94       *                      expressed in nanoseconds.
95       * @param accelerationX linear acceleration along x-axis expressed in meters
96       *                      per squared second (m/s^2).
97       * @param accelerationY linear acceleration along y-axis expressed in meters
98       *                      per squared second (m/s^2).
99       * @param accelerationZ linear acceleration along z-axis expressed in meters
100      *                      per squared second (m/s^2).
101      */
102     public void updateAccelerometerSample(final long timestamp, final float accelerationX,
103                                           final float accelerationY, final float accelerationZ) {
104         if (lastViewPairTimestamp < 0) {
105             lastViewPairTimestamp = timestamp;
106         }
107 
108         if (slamEstimator != null) {
109             slamEstimator.updateAccelerometerSample(timestamp - lastViewPairTimestamp,
110                     accelerationX, accelerationY, accelerationZ);
111         }
112         lastTimestamp = timestamp;
113     }
114 
115     /**
116      * Provides a new accelerometer sample to update SLAM estimation.
117      * This method must be called whenever the accelerometer sensor receives new
118      * data.
119      * If re-constructor is not running, calling this method has no effect.
120      *
121      * @param timestamp timestamp of accelerometer sample since epoch time and
122      *                  expressed in nanoseconds.
123      * @param data      array containing x,y,z components of linear acceleration
124      *                  expressed in meters per squared second (m/s^2).
125      * @throws IllegalArgumentException if provided array does not have length
126      *                                  3.
127      */
128     public void updateAccelerometerSample(final long timestamp, final float[] data) {
129         if (lastViewPairTimestamp < 0) {
130             lastViewPairTimestamp = timestamp;
131         }
132 
133         if (slamEstimator != null) {
134             slamEstimator.updateAccelerometerSample(timestamp - lastViewPairTimestamp, data);
135         }
136         lastTimestamp = timestamp;
137     }
138 
139     /**
140      * Provides a new gyroscope sample to update SLAM estimation.
141      * If re-constructor is not running, calling this method has no effect.
142      *
143      * @param timestamp     timestamp of gyroscope sample since epoch time and
144      *                      expressed in nanoseconds.
145      * @param angularSpeedX angular speed of rotation along x-axis expressed in
146      *                      radians per second (rad/s).
147      * @param angularSpeedY angular speed of rotation along y-axis expressed in
148      *                      radians per second (rad/s).
149      * @param angularSpeedZ angular speed of rotation along z-axis expressed in
150      *                      radians per second (rad/s).
151      */
152     public void updateGyroscopeSample(final long timestamp, final float angularSpeedX,
153                                       final float angularSpeedY, final float angularSpeedZ) {
154         if (lastViewPairTimestamp < 0) {
155             lastViewPairTimestamp = timestamp;
156         }
157 
158         if (slamEstimator != null) {
159             slamEstimator.updateGyroscopeSample(timestamp - lastViewPairTimestamp,
160                     angularSpeedX, angularSpeedY, angularSpeedZ);
161         }
162         lastTimestamp = timestamp;
163     }
164 
165     /**
166      * Provides a new gyroscope sample to update SLAM estimation.
167      * If re-constructor is not running, calling this method has no effect.
168      *
169      * @param timestamp timestamp of gyroscope sample since epoch time and
170      *                  expressed in nanoseconds.
171      * @param data      angular speed of rotation along x,y,z axes expressed in
172      *                  radians per second (rad/s).
173      * @throws IllegalArgumentException if provided array does not have length
174      *                                  3.
175      */
176     public void updateGyroscopeSample(final long timestamp, final float[] data) {
177         if (lastViewPairTimestamp < 0) {
178             lastViewPairTimestamp = timestamp;
179         }
180 
181         if (slamEstimator != null) {
182             slamEstimator.updateGyroscopeSample(timestamp - lastViewPairTimestamp, data);
183         }
184         lastTimestamp = timestamp;
185     }
186 
187     /**
188      * Resets this instance so that a reconstruction can be started from the beginning without
189      * cancelling current one.
190      */
191     @Override
192     public void reset() {
193         super.reset();
194         lastTimestamp = lastViewPairTimestamp = -1;
195     }
196 
197     /**
198      * Indicates whether implementations of a re-constructor uses absolute orientation or
199      * not.
200      *
201      * @return true if absolute orientation is used, false, otherwise.
202      */
203     @Override
204     protected boolean hasAbsoluteOrientation() {
205         return false;
206     }
207 
208 
209     /**
210      * Configures calibration data on SLAM estimator if available.
211      */
212     protected void setUpCalibrationData() {
213         final var calibrationData = configuration.getCalibrationData();
214         if (calibrationData != null) {
215             slamEstimator.setCalibrationData(calibrationData);
216         }
217     }
218 
219     /**
220      * Configures listener of SLAM estimator
221      */
222     protected void setUpSlamEstimatorListener() {
223         slamEstimator.setListener(new BaseSlamEstimator.BaseSlamEstimatorListener<>() {
224             @Override
225             public void onFullSampleReceived(final BaseSlamEstimator<D> estimator) {
226                 // not used
227             }
228 
229             @Override
230             public void onFullSampleProcessed(final BaseSlamEstimator<D> estimator) {
231                 notifySlamStateAndCamera();
232             }
233 
234             @Override
235             public void onCorrectWithPositionMeasure(final BaseSlamEstimator<D> estimator) {
236                 // not used
237             }
238 
239             @Override
240             public void onCorrectedWithPositionMeasure(final BaseSlamEstimator<D> estimator) {
241                 notifySlamStateAndCamera();
242             }
243 
244             private void notifySlamStateAndCamera() {
245                 notifySlamStateIfNeeded();
246                 notifySlamCameraIfNeeded();
247             }
248         });
249     }
250 
251     /**
252      * Transforms metric cameras on current pair of views so that they are referred to
253      * last kept location and rotation and upgrades cameras from metric stratum to
254      * Euclidean stratum.
255      *
256      * @param isInitialPairOfViews   true if initial pair of views is being processed, false otherwise.
257      * @param hasAbsoluteOrientation true if absolute orientation is required, false otherwise.
258      * @return true if cameras were successfully transformed.
259      */
260     @Override
261     protected boolean transformPairOfCamerasAndPoints(final boolean isInitialPairOfViews,
262                                                       final boolean hasAbsoluteOrientation) {
263         final var previousMetricCamera = previousMetricEstimatedCamera.getCamera();
264         final var currentMetricCamera = currentMetricEstimatedCamera.getCamera();
265         if (previousMetricCamera == null || currentMetricCamera == null) {
266             return false;
267         }
268 
269         currentScale = estimateCurrentScale();
270         final var sqrScale = currentScale * currentScale;
271 
272         referenceEuclideanTransformation = new MetricTransformation3D(currentScale);
273         if (hasAbsoluteOrientation) {
274             invEuclideanCameraRotation = lastEuclideanCameraRotation.inverseRotationAndReturnNew();
275             if (isInitialPairOfViews) {
276                 referenceEuclideanTransformation.setRotation(invEuclideanCameraRotation);
277             }
278         }
279 
280         if (!isInitialPairOfViews) {
281             // additional pairs also need to translate and rotate
282             if (invEuclideanCameraRotation == null) {
283                 invEuclideanCameraRotation = lastEuclideanCameraRotation.inverseRotationAndReturnNew();
284             } else {
285                 lastEuclideanCameraRotation.inverseRotation(invEuclideanCameraRotation);
286             }
287             referenceEuclideanTransformation.setRotation(invEuclideanCameraRotation);
288             referenceEuclideanTransformation.setTranslation(lastEuclideanCameraCenter);
289         }
290 
291         try {
292             // transform cameras
293             final var previousEuclideanCamera = referenceEuclideanTransformation.transformAndReturnNew(
294                     previousMetricCamera);
295             final var currentEuclideanCamera = referenceEuclideanTransformation.transformAndReturnNew(
296                     currentMetricCamera);
297 
298             previousEuclideanEstimatedCamera = new EstimatedCamera();
299             previousEuclideanEstimatedCamera.setCamera(previousEuclideanCamera);
300             previousEuclideanEstimatedCamera.setViewId(previousMetricEstimatedCamera.getViewId());
301             previousEuclideanEstimatedCamera.setQualityScore(previousMetricEstimatedCamera.getQualityScore());
302             if (previousMetricEstimatedCamera.getCovariance() != null) {
303                 previousEuclideanEstimatedCamera.setCovariance(previousMetricEstimatedCamera.getCovariance()
304                         .multiplyByScalarAndReturnNew(sqrScale));
305             }
306 
307             currentEuclideanEstimatedCamera = new EstimatedCamera();
308             currentEuclideanEstimatedCamera.setCamera(currentEuclideanCamera);
309             currentEuclideanEstimatedCamera.setViewId(currentMetricEstimatedCamera.getViewId());
310             currentEuclideanEstimatedCamera.setQualityScore(currentMetricEstimatedCamera.getQualityScore());
311             if (currentMetricEstimatedCamera.getCovariance() != null) {
312                 currentEuclideanEstimatedCamera.setCovariance(currentMetricEstimatedCamera.getCovariance()
313                         .multiplyByScalarAndReturnNew(sqrScale));
314             }
315 
316             // transform points
317             euclideanReconstructedPoints = new ArrayList<>();
318             ReconstructedPoint3D euclideanReconstructedPoint;
319             for (final ReconstructedPoint3D metricReconstructedPoint : metricReconstructedPoints) {
320                 final var metricPoint = metricReconstructedPoint.getPoint();
321                 final var euclideanPoint = referenceEuclideanTransformation.transformAndReturnNew(metricPoint);
322                 euclideanReconstructedPoint = new ReconstructedPoint3D();
323                 euclideanReconstructedPoint.setPoint(euclideanPoint);
324                 euclideanReconstructedPoint.setInlier(metricReconstructedPoint.isInlier());
325                 euclideanReconstructedPoint.setId(metricReconstructedPoint.getId());
326                 euclideanReconstructedPoint.setColorData(metricReconstructedPoint.getColorData());
327                 if (metricReconstructedPoint.getCovariance() != null) {
328                     euclideanReconstructedPoint.setCovariance(metricReconstructedPoint.getCovariance()
329                             .multiplyByScalarAndReturnNew(sqrScale));
330                 }
331                 euclideanReconstructedPoint.setQualityScore(metricReconstructedPoint.getQualityScore());
332                 euclideanReconstructedPoints.add(euclideanReconstructedPoint);
333             }
334 
335         } catch (final AlgebraException e) {
336             return false;
337         }
338 
339         return super.transformPairOfCamerasAndPoints(isInitialPairOfViews, hasAbsoluteOrientation);
340     }
341 
342     /**
343      * Estimates current scale using SLAM data.
344      *
345      * @return estimated scale.
346      */
347     private double estimateCurrentScale() {
348         try {
349             final var metricCamera1 = previousMetricEstimatedCamera.getCamera();
350             final var metricCamera2 = currentMetricEstimatedCamera.getCamera();
351 
352             if (!metricCamera1.isCameraCenterAvailable()) {
353                 metricCamera1.decompose(false, true);
354             }
355             if (!metricCamera2.isCameraCenterAvailable()) {
356                 metricCamera2.decompose(false, true);
357             }
358 
359             final var metricCenter1 = metricCamera1.getCameraCenter();
360             final var metricCenter2 = metricCamera2.getCameraCenter();
361 
362             // obtain baseline (camera separation from slam estimator data)
363             final var slamPosX = slamEstimator.getStatePositionX();
364             final var slamPosY = slamEstimator.getStatePositionY();
365             final var slamPosZ = slamEstimator.getStatePositionZ();
366 
367             slamPosition.setInhomogeneousCoordinates(slamPosX, slamPosY, slamPosZ);
368 
369             // we assume that Euclidean center of 1st camera is at origin because by the time
370             // this method is called, metric cameras have not yet been orientation and position
371             // transformed
372             final var euclideanBaseline = Math.sqrt(slamPosX * slamPosX + slamPosY * slamPosY + slamPosZ * slamPosZ);
373             final var metricBaseline = metricCenter1.distanceTo(metricCenter2);
374 
375             // because when we call reset in SLAM estimator, timestamp is lost, we keep
376             // track of last timestamp to be subtracted on subsequent update calls
377             lastViewPairTimestamp = lastTimestamp;
378 
379             // reset linear velocity and orientation and keep other slam state parameters
380             slamEstimator.resetPositionAndVelocity();
381 
382             return euclideanBaseline / metricBaseline;
383         } catch (final Exception e) {
384             failed = true;
385             //noinspection unchecked
386             listener.onFail((R) this);
387             return DEFAULT_SCALE;
388         }
389     }
390 
391     /**
392      * Notifies SLAM state if notification is enabled at configuration time.
393      */
394     private void notifySlamStateIfNeeded() {
395         if (!configuration.isNotifyAvailableSlamDataEnabled()) {
396             return;
397         }
398 
399         final var lastPosX = lastEuclideanCameraCenter != null ? lastEuclideanCameraCenter.getInhomX() : 0.0;
400         final var lastPosY = lastEuclideanCameraCenter != null ? lastEuclideanCameraCenter.getInhomY() : 0.0;
401         final var lastPosZ = lastEuclideanCameraCenter != null ? lastEuclideanCameraCenter.getInhomZ() : 0.0;
402 
403         final var positionX = lastPosX + slamEstimator.getStatePositionX();
404         final var positionY = lastPosY + slamEstimator.getStatePositionY();
405         final var positionZ = lastPosZ + slamEstimator.getStatePositionZ();
406 
407         final var velocityX = slamEstimator.getStateVelocityX();
408         final var velocityY = slamEstimator.getStateVelocityY();
409         final var velocityZ = slamEstimator.getStateVelocityZ();
410 
411         final var accelerationX = slamEstimator.getStateAccelerationX();
412         final var accelerationY = slamEstimator.getStateAccelerationY();
413         final var accelerationZ = slamEstimator.getStateAccelerationZ();
414 
415         final var quaternionA = slamEstimator.getStateQuaternionA();
416         final var quaternionB = slamEstimator.getStateQuaternionB();
417         final var quaternionC = slamEstimator.getStateQuaternionC();
418         final var quaternionD = slamEstimator.getStateQuaternionD();
419 
420         final var angularSpeedX = slamEstimator.getStateAngularSpeedX();
421         final var angularSpeedY = slamEstimator.getStateAngularSpeedY();
422         final var angularSpeedZ = slamEstimator.getStateAngularSpeedZ();
423 
424         //noinspection unchecked
425         listener.onSlamDataAvailable((R) this, positionX, positionY, positionZ, velocityX, velocityY, velocityZ,
426                 accelerationX, accelerationY, accelerationZ, quaternionA, quaternionB, quaternionC, quaternionD,
427                 angularSpeedX, angularSpeedY, angularSpeedZ, slamEstimator.getStateCovariance());
428     }
429 
430     /**
431      * Notifies estimated camera by means of SLAM if notification is enabled at
432      * configuration time and intrinsics are already available.
433      */
434     private void notifySlamCameraIfNeeded() {
435         if (!configuration.isNotifyEstimatedSlamCameraEnabled()) {
436             return;
437         }
438 
439         // try with current camera
440         var camera = currentEuclideanEstimatedCamera != null ? currentEuclideanEstimatedCamera.getCamera() : null;
441         if (camera == null) {
442             // if not available try with previous camera
443             camera = previousEuclideanEstimatedCamera != null ? previousEuclideanEstimatedCamera.getCamera() : null;
444         }
445 
446         try {
447             PinholeCameraIntrinsicParameters intrinsicParameters = null;
448             if (camera != null) {
449                 if (!camera.areIntrinsicParametersAvailable()) {
450                     // decompose camera to obtain intrinsic parameters
451                     camera.decompose();
452                 }
453 
454                 intrinsicParameters = camera.getIntrinsicParameters();
455             } else if (configuration.areIntrinsicParametersKnown()) {
456                 //noinspection unchecked
457                 intrinsicParameters = listener.onIntrinsicParametersRequested((R) this, currentViewId);
458             }
459 
460             if (intrinsicParameters == null) {
461                 return;
462             }
463 
464             final var lastPosX = lastEuclideanCameraCenter != null ? lastEuclideanCameraCenter.getInhomX() : 0.0;
465             final var lastPosY = lastEuclideanCameraCenter != null ? lastEuclideanCameraCenter.getInhomY() : 0.0;
466             final var lastPosZ = lastEuclideanCameraCenter != null ? lastEuclideanCameraCenter.getInhomZ() : 0.0;
467 
468             final var positionX = lastPosX + slamEstimator.getStatePositionX();
469             final var positionY = lastPosY + slamEstimator.getStatePositionY();
470             final var positionZ = lastPosZ + slamEstimator.getStatePositionZ();
471             slamPosition.setInhomogeneousCoordinates(positionX, positionY, positionZ);
472 
473             final var quaternionA = slamEstimator.getStateQuaternionA();
474             final var quaternionB = slamEstimator.getStateQuaternionB();
475             final var quaternionC = slamEstimator.getStateQuaternionC();
476             final var quaternionD = slamEstimator.getStateQuaternionD();
477             slamRotation.setA(quaternionA);
478             slamRotation.setB(quaternionB);
479             slamRotation.setC(quaternionC);
480             slamRotation.setD(quaternionD);
481 
482             slamCamera.setIntrinsicAndExtrinsicParameters(intrinsicParameters, slamRotation, slamPosition);
483 
484             //noinspection unchecked
485             listener.onSlamCameraEstimated((R) this, slamCamera);
486 
487         } catch (final GeometryException ignore) {
488             // do nothing
489         }
490     }
491 }